Skip to main content

fastapi_core/
dependency.rs

1//! Dependency injection support.
2//!
3//! This module provides the `Depends` extractor and supporting types for
4//! request-scoped dependency resolution with optional caching and overrides.
5//!
6//! # Circular Dependency Detection
7//!
8//! The DI system detects circular dependencies at runtime. If a dependency
9//! graph contains a cycle (e.g., A -> B -> C -> A), the resolution will fail
10//! with a [`CircularDependencyError`] containing the full cycle path.
11//!
12//! ```text
13//! Circular dependency detected: DbPool -> UserService -> AuthService -> DbPool
14//! ```
15//!
16//! # Configuration Errors Cause Panics
17//!
18//! Circular dependencies and scope violations are **configuration errors**
19//! that indicate a bug in the dependency graph setup. These errors cause
20//! panics rather than returning `Result` for the following reasons:
21//!
22//! 1. **Type safety**: The error type is `T::Error` (the dependency's error type),
23//!    which may not be constructible from internal errors like `CircularDependencyError`.
24//!
25//! 2. **Fail fast**: Configuration errors should crash early during development/testing,
26//!    not be silently caught and potentially ignored.
27//!
28//! 3. **Invariant violation**: A circular dependency means the dependency graph
29//!    is fundamentally broken and cannot be satisfied.
30//!
31//! To debug circular dependencies:
32//! - Review the panic message which shows the full cycle path
33//! - Check for unintended transitive dependencies
34//! - Consider using scoped dependencies to break cycles
35
36use crate::context::RequestContext;
37use crate::extract::FromRequest;
38use crate::request::Request;
39use crate::response::{IntoResponse, Response, ResponseBody, StatusCode};
40use parking_lot::Mutex;
41use parking_lot::RwLock;
42use std::any::{Any, TypeId};
43use std::collections::HashMap;
44use std::future::Future;
45use std::marker::PhantomData;
46use std::ops::{Deref, DerefMut};
47use std::pin::Pin;
48use std::sync::Arc;
49
50/// Dependency resolution scope.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum DependencyScope {
53    /// No request-level caching; resolve on each call.
54    Function,
55    /// Cache for the lifetime of the request.
56    Request,
57}
58
59// ============================================================================
60// Cleanup Stack (for Generator-style Dependencies)
61// ============================================================================
62
63/// Type alias for cleanup functions.
64///
65/// A cleanup function is an async closure that performs teardown work after
66/// the request handler completes. Cleanup functions run in LIFO (last-in,
67/// first-out) order, similar to Python's `contextlib.ExitStack`.
68pub type CleanupFn = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
69
70/// Stack of cleanup functions to run after handler completion.
71///
72/// `CleanupStack` provides generator-style dependency lifecycle management
73/// similar to FastAPI's `yield` dependencies. Cleanup functions are registered
74/// during dependency resolution and run in LIFO order after the handler
75/// completes, even on error or panic.
76///
77/// # Example
78///
79/// ```ignore
80/// // Dependency with cleanup
81/// impl FromDependencyWithCleanup for DbConnection {
82///     type Value = DbConnection;
83///     type Error = HttpError;
84///
85///     async fn setup(ctx: &RequestContext, req: &mut Request)
86///         -> Result<(Self::Value, Option<CleanupFn>), Self::Error>
87///     {
88///         let conn = DbPool::get_connection().await?;
89///         let cleanup = {
90///             let conn = conn.clone();
91///             Box::new(move || {
92///                 Box::pin(async move {
93///                     conn.release().await;
94///                 }) as Pin<Box<dyn Future<Output = ()> + Send>>
95///             }) as CleanupFn
96///         };
97///         Ok((conn, Some(cleanup)))
98///     }
99/// }
100/// ```
101///
102/// # Cleanup Order
103///
104/// Cleanup functions run in LIFO order (last registered runs first):
105///
106/// ```text
107/// Setup order:   A -> B -> C
108/// Cleanup order: C -> B -> A
109/// ```
110///
111/// This ensures that dependencies are cleaned up in the reverse order
112/// of their setup, maintaining proper resource lifecycle semantics.
113pub struct CleanupStack {
114    /// Cleanup functions in registration order (will be reversed when running).
115    cleanups: Mutex<Vec<CleanupFn>>,
116}
117
118impl CleanupStack {
119    /// Create an empty cleanup stack.
120    #[must_use]
121    pub fn new() -> Self {
122        Self {
123            cleanups: Mutex::new(Vec::new()),
124        }
125    }
126
127    /// Register a cleanup function to run after handler completion.
128    ///
129    /// Cleanup functions are run in LIFO order (last registered runs first).
130    pub fn push(&self, cleanup: CleanupFn) {
131        let mut guard = self.cleanups.lock();
132        guard.push(cleanup);
133    }
134
135    /// Take all cleanup functions for execution.
136    ///
137    /// Returns cleanups in LIFO order (reversed from registration order).
138    /// After calling this, the stack is empty.
139    pub fn take_cleanups(&self) -> Vec<CleanupFn> {
140        let mut guard = self.cleanups.lock();
141        let mut cleanups = std::mem::take(&mut *guard);
142        cleanups.reverse(); // LIFO order
143        cleanups
144    }
145
146    /// Returns the number of registered cleanup functions.
147    #[must_use]
148    pub fn len(&self) -> usize {
149        let guard = self.cleanups.lock();
150        guard.len()
151    }
152
153    /// Returns true if no cleanup functions are registered.
154    #[must_use]
155    pub fn is_empty(&self) -> bool {
156        self.len() == 0
157    }
158
159    /// Run all cleanup functions in LIFO order.
160    ///
161    /// This consumes all registered cleanup functions. Each cleanup is
162    /// awaited in sequence. If a cleanup function panics (either during
163    /// creation or execution), the remaining cleanups are still attempted.
164    ///
165    /// # Returns
166    ///
167    /// The number of cleanup functions that completed successfully.
168    pub async fn run_cleanups(&self) -> usize {
169        let cleanups = self.take_cleanups();
170        let mut completed = 0;
171
172        for cleanup in cleanups {
173            // Call the cleanup function to get the future, catching panics during creation
174            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (cleanup)()));
175            match result {
176                Ok(future) => {
177                    // Run the future, catching any panics during execution
178                    if Self::run_cleanup_future(future).await {
179                        completed += 1;
180                    }
181                    // If cleanup panicked during execution, continue with remaining
182                }
183                Err(_) => {
184                    // Cleanup panicked during creation, continue with remaining cleanups
185                }
186            }
187        }
188
189        completed
190    }
191
192    /// Run a single cleanup future, catching any panics during execution.
193    ///
194    /// Returns `true` if the cleanup completed successfully, `false` if it panicked.
195    async fn run_cleanup_future(mut future: Pin<Box<dyn Future<Output = ()> + Send>>) -> bool {
196        use std::panic::{AssertUnwindSafe, catch_unwind};
197        use std::task::Poll;
198
199        std::future::poll_fn(move |cx| {
200            match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(cx))) {
201                Ok(Poll::Ready(())) => Poll::Ready(true),
202                Ok(Poll::Pending) => Poll::Pending,
203                Err(_) => Poll::Ready(false), // Cleanup panicked, treat as failed
204            }
205        })
206        .await
207    }
208}
209
210impl Default for CleanupStack {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl std::fmt::Debug for CleanupStack {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("CleanupStack")
219            .field("count", &self.len())
220            .finish()
221    }
222}
223
224/// Trait for dependencies that require cleanup after handler completion.
225///
226/// This is similar to FastAPI's `yield` dependencies. Dependencies implementing
227/// this trait can perform setup logic and register cleanup functions that
228/// run after the request handler completes.
229///
230/// # Example
231///
232/// ```ignore
233/// struct DbTransaction {
234///     tx: Transaction,
235/// }
236///
237/// impl FromDependencyWithCleanup for DbTransaction {
238///     type Value = DbTransaction;
239///     type Error = HttpError;
240///
241///     async fn setup(ctx: &RequestContext, req: &mut Request)
242///         -> Result<(Self::Value, Option<CleanupFn>), Self::Error>
243///     {
244///         let pool = Depends::<DbPool>::from_request(ctx, req).await?;
245///         let tx = pool.begin().await.map_err(|_| HttpError::internal())?;
246///
247///         let cleanup_tx = tx.clone();
248///         let cleanup = Box::new(move || {
249///             Box::pin(async move {
250///                 // Commit or rollback happens here
251///                 cleanup_tx.commit().await.ok();
252///             }) as Pin<Box<dyn Future<Output = ()> + Send>>
253///         }) as CleanupFn;
254///
255///         Ok((DbTransaction { tx }, Some(cleanup)))
256///     }
257/// }
258/// ```
259pub trait FromDependencyWithCleanup: Clone + Send + Sync + 'static {
260    /// The value type produced by setup.
261    type Value: Clone + Send + Sync + 'static;
262    /// Error type when setup fails.
263    type Error: IntoResponse + Send + Sync + 'static;
264
265    /// Set up the dependency and optionally return a cleanup function.
266    ///
267    /// The cleanup function (if provided) will run after the request handler
268    /// completes, even on error or panic.
269    fn setup(
270        ctx: &RequestContext,
271        req: &mut Request,
272    ) -> impl Future<Output = Result<(Self::Value, Option<CleanupFn>), Self::Error>> + Send;
273}
274
275/// Wrapper for dependencies that have cleanup callbacks.
276///
277/// `DependsCleanup` works like `Depends` but for types implementing
278/// `FromDependencyWithCleanup`. It automatically registers cleanup
279/// functions with the request's cleanup stack.
280#[derive(Debug, Clone)]
281pub struct DependsCleanup<T, C = DefaultDependencyConfig>(pub T, PhantomData<C>);
282
283impl<T, C> DependsCleanup<T, C> {
284    /// Create a new `DependsCleanup` wrapper.
285    #[must_use]
286    pub fn new(value: T) -> Self {
287        Self(value, PhantomData)
288    }
289
290    /// Unwrap the inner value.
291    #[must_use]
292    pub fn into_inner(self) -> T {
293        self.0
294    }
295}
296
297impl<T, C> Deref for DependsCleanup<T, C> {
298    type Target = T;
299
300    fn deref(&self) -> &Self::Target {
301        &self.0
302    }
303}
304
305impl<T, C> DerefMut for DependsCleanup<T, C> {
306    fn deref_mut(&mut self) -> &mut Self::Target {
307        &mut self.0
308    }
309}
310
311impl<T, C> FromRequest for DependsCleanup<T, C>
312where
313    T: FromDependencyWithCleanup<Value = T>,
314    C: DependsConfig,
315{
316    type Error = T::Error;
317
318    async fn from_request(ctx: &RequestContext, req: &mut Request) -> Result<Self, Self::Error> {
319        // Check cancellation before resolving dependency
320        let _ = ctx.checkpoint();
321
322        let scope = C::SCOPE.unwrap_or(DependencyScope::Request);
323        let use_cache = C::USE_CACHE && scope == DependencyScope::Request;
324
325        // Check cache first
326        if use_cache {
327            if let Some(cached) = ctx.dependency_cache().get::<T::Value>() {
328                return Ok(DependsCleanup::new(cached));
329            }
330        }
331
332        // Check for circular dependency (bd-276p: intentional panic for config errors)
333        let type_name = std::any::type_name::<T>();
334        if let Some(cycle) = ctx.resolution_stack().check_cycle::<T>(type_name) {
335            handle_circular_dependency(cycle);
336        }
337
338        // Check for scope violation: request-scoped cannot depend on function-scoped
339        if let Some(scope_err) = ctx
340            .resolution_stack()
341            .check_scope_violation(type_name, scope)
342        {
343            handle_scope_violation(scope_err);
344        }
345
346        // Push onto resolution stack
347        ctx.resolution_stack().push::<T>(type_name, scope);
348        let _guard = ResolutionGuard::new(ctx.resolution_stack());
349
350        // Setup the dependency
351        let _ = ctx.checkpoint();
352        let (value, cleanup) = T::setup(ctx, req).await?;
353
354        // Register cleanup if provided
355        if let Some(cleanup_fn) = cleanup {
356            ctx.cleanup_stack().push(cleanup_fn);
357        }
358
359        // Cache if needed
360        if use_cache {
361            ctx.dependency_cache().insert::<T::Value>(value.clone());
362        }
363
364        Ok(DependsCleanup::new(value))
365    }
366}
367
368/// Configuration for `Depends` resolution.
369pub trait DependsConfig {
370    /// Whether to use caching.
371    const USE_CACHE: bool;
372    /// Optional scope override.
373    const SCOPE: Option<DependencyScope>;
374}
375
376/// Default dependency configuration (cache per request).
377#[derive(Debug, Clone, Copy)]
378pub struct DefaultDependencyConfig;
379
380/// Backwards-friendly alias for the default config.
381pub type DefaultConfig = DefaultDependencyConfig;
382
383impl DependsConfig for DefaultDependencyConfig {
384    const USE_CACHE: bool = true;
385    const SCOPE: Option<DependencyScope> = None;
386}
387
388/// Disable caching for this dependency.
389#[derive(Debug, Clone, Copy)]
390pub struct NoCache;
391
392impl DependsConfig for NoCache {
393    const USE_CACHE: bool = false;
394    const SCOPE: Option<DependencyScope> = Some(DependencyScope::Function);
395}
396
397// ============================================================================
398// Circular Dependency Detection
399// ============================================================================
400
401/// Error returned when a circular dependency is detected.
402///
403/// This error contains the full cycle path showing which types form the cycle.
404/// For example, if `DbPool` depends on `UserService` which depends on `DbPool`,
405/// the cycle would be: `["DbPool", "UserService", "DbPool"]`.
406///
407/// # Example
408///
409/// ```text
410/// Circular dependency detected: DbPool -> UserService -> DbPool
411/// ```
412#[derive(Debug, Clone)]
413pub struct CircularDependencyError {
414    /// The names of the types forming the cycle, in resolution order.
415    /// The first and last element are the same type (completing the cycle).
416    pub cycle: Vec<String>,
417}
418
419/// Error returned when a dependency scope constraint is violated.
420///
421/// This occurs when a request-scoped dependency depends on a function-scoped
422/// dependency. Since request-scoped dependencies are cached for the lifetime
423/// of the request, they would hold stale values from function-scoped
424/// dependencies (which should be resolved fresh on each call).
425///
426/// # Example
427///
428/// ```text
429/// Dependency scope violation: request-scoped 'CachedUser' depends on
430/// function-scoped 'DbConnection'. Request-scoped dependencies cannot
431/// depend on function-scoped dependencies because the cached value
432/// would become stale.
433/// ```
434///
435/// # Why This Matters
436///
437/// Consider this scenario:
438/// - `CachedUser` is request-scoped (cached for the request)
439/// - `DbConnection` is function-scoped (fresh connection each call)
440///
441/// If `CachedUser` depends on `DbConnection`:
442/// 1. First request: `CachedUser` resolved, caches `DbConnection` value
443/// 2. Later in same request: `CachedUser` retrieved from cache
444/// 3. Problem: The cached `CachedUser` holds a stale `DbConnection`
445///
446/// This violates the contract that function-scoped dependencies are fresh.
447#[derive(Debug, Clone)]
448pub struct DependencyScopeError {
449    /// The name of the request-scoped dependency (the outer one).
450    pub request_scoped_type: String,
451    /// The name of the function-scoped dependency (the inner one).
452    pub function_scoped_type: String,
453}
454
455impl CircularDependencyError {
456    /// Create a new circular dependency error with the given cycle path.
457    #[must_use]
458    pub fn new(cycle: Vec<String>) -> Self {
459        Self { cycle }
460    }
461
462    /// Get a human-readable representation of the cycle.
463    #[must_use]
464    pub fn cycle_path(&self) -> String {
465        self.cycle.join(" -> ")
466    }
467}
468
469impl std::fmt::Display for CircularDependencyError {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        write!(f, "Circular dependency detected: {}", self.cycle_path())
472    }
473}
474
475impl std::error::Error for CircularDependencyError {}
476
477impl IntoResponse for CircularDependencyError {
478    fn into_response(self) -> Response {
479        let body = format!(
480            r#"{{"detail":"Circular dependency detected: {}"}}"#,
481            self.cycle_path()
482        );
483        Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
484            .header("content-type", b"application/json".to_vec())
485            .body(ResponseBody::Bytes(body.into_bytes()))
486    }
487}
488
489impl DependencyScopeError {
490    /// Create a new scope violation error.
491    #[must_use]
492    pub fn new(request_scoped_type: String, function_scoped_type: String) -> Self {
493        Self {
494            request_scoped_type,
495            function_scoped_type,
496        }
497    }
498}
499
500impl std::fmt::Display for DependencyScopeError {
501    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502        write!(
503            f,
504            "Dependency scope violation: request-scoped '{}' depends on function-scoped '{}'. \
505             Request-scoped dependencies cannot depend on function-scoped dependencies \
506             because the cached value would become stale.",
507            self.request_scoped_type, self.function_scoped_type
508        )
509    }
510}
511
512impl std::error::Error for DependencyScopeError {}
513
514impl IntoResponse for DependencyScopeError {
515    fn into_response(self) -> Response {
516        let body = format!(
517            r#"{{"detail":"Dependency scope violation: request-scoped '{}' depends on function-scoped '{}'. Request-scoped dependencies cannot depend on function-scoped dependencies."}}"#,
518            self.request_scoped_type, self.function_scoped_type
519        );
520        Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
521            .header("content-type", b"application/json".to_vec())
522            .body(ResponseBody::Bytes(body.into_bytes()))
523    }
524}
525
526/// Handle a detected circular dependency by panicking with a helpful message.
527///
528/// This is marked `#[cold]` to hint to the compiler that this path is unlikely,
529/// and `#[inline(never)]` to keep the panic code out of the hot path.
530///
531/// # Panics
532///
533/// Always panics with a detailed error message including the cycle path and debugging hints.
534#[cold]
535#[inline(never)]
536fn handle_circular_dependency(cycle: Vec<String>) -> ! {
537    let err = CircularDependencyError::new(cycle);
538    panic!(
539        "\n\n\
540        ╔══════════════════════════════════════════════════════════════════════╗\n\
541        ║                    CIRCULAR DEPENDENCY DETECTED                      ║\n\
542        ╠══════════════════════════════════════════════════════════════════════╣\n\
543        ║ {}\n\
544        ╠══════════════════════════════════════════════════════════════════════╣\n\
545        ║ This is a configuration error in your dependency graph.              ║\n\
546        ║                                                                      ║\n\
547        ║ To fix:                                                              ║\n\
548        ║ 1. Review the cycle path above                                       ║\n\
549        ║ 2. Break the cycle by removing or refactoring one dependency         ║\n\
550        ║ 3. Consider using lazy initialization or scoped dependencies         ║\n\
551        ╚══════════════════════════════════════════════════════════════════════╝\n",
552        err
553    );
554}
555
556/// Handle a detected scope violation by panicking with a helpful message.
557///
558/// This is marked `#[cold]` to hint to the compiler that this path is unlikely,
559/// and `#[inline(never)]` to keep the panic code out of the hot path.
560///
561/// # Panics
562///
563/// Always panics with a detailed error message explaining the scope violation.
564#[cold]
565#[inline(never)]
566fn handle_scope_violation(scope_err: DependencyScopeError) -> ! {
567    panic!(
568        "\n\n\
569        ╔══════════════════════════════════════════════════════════════════════╗\n\
570        ║                    DEPENDENCY SCOPE VIOLATION                        ║\n\
571        ╠══════════════════════════════════════════════════════════════════════╣\n\
572        ║ {}\n\
573        ╠══════════════════════════════════════════════════════════════════════╣\n\
574        ║ Request-scoped dependencies are cached per-request and must not      ║\n\
575        ║ depend on function-scoped dependencies (which are created fresh      ║\n\
576        ║ each time).                                                          ║\n\
577        ║                                                                      ║\n\
578        ║ To fix:                                                              ║\n\
579        ║ 1. Change the inner dependency to request scope                      ║\n\
580        ║ 2. Or change the outer dependency to function scope                  ║\n\
581        ║ 3. Or restructure to avoid the dependency                            ║\n\
582        ╚══════════════════════════════════════════════════════════════════════╝\n",
583        scope_err
584    );
585}
586
587/// Tracks which types are currently being resolved to detect cycles and scope violations.
588///
589/// This uses a stack-based approach: when we start resolving a type, we push
590/// its `TypeId`, name, and scope onto the stack. If we see the same `TypeId` again
591/// before finishing, we have a cycle. Additionally, if a request-scoped dependency
592/// tries to resolve a function-scoped dependency, we detect a scope violation.
593pub struct ResolutionStack {
594    /// Stack of (TypeId, type_name, scope) tuples currently being resolved.
595    stack: RwLock<Vec<(TypeId, String, DependencyScope)>>,
596}
597
598impl ResolutionStack {
599    /// Create an empty resolution stack.
600    #[must_use]
601    pub fn new() -> Self {
602        Self {
603            stack: RwLock::new(Vec::new()),
604        }
605    }
606
607    /// Check if a type is currently being resolved (would form a cycle).
608    ///
609    /// Returns `Some(cycle_path)` if the type is already on the stack,
610    /// or `None` if it's safe to proceed.
611    pub fn check_cycle<T: 'static>(&self, type_name: &str) -> Option<Vec<String>> {
612        let type_id = TypeId::of::<T>();
613        let guard = self.stack.read();
614
615        // Check if this type is already being resolved
616        if let Some(pos) = guard.iter().position(|(id, _, _)| *id == type_id) {
617            // Build the cycle path from the position where we first saw this type
618            let mut cycle: Vec<String> = guard[pos..]
619                .iter()
620                .map(|(_, name, _)| name.clone())
621                .collect();
622            // Add the current type to complete the cycle
623            cycle.push(type_name.to_owned());
624            return Some(cycle);
625        }
626
627        None
628    }
629
630    /// Check for scope violations.
631    ///
632    /// Returns `Some(DependencyScopeError)` if there is a request-scoped dependency
633    /// on the stack and we're trying to resolve a function-scoped dependency.
634    ///
635    /// # Scope Rules
636    ///
637    /// - Request-scoped can depend on request-scoped (both cached, OK)
638    /// - Function-scoped can depend on function-scoped (both fresh, OK)
639    /// - Function-scoped can depend on request-scoped (inner cached, outer fresh, OK)
640    /// - Request-scoped CANNOT depend on function-scoped (outer cached with stale inner, BAD)
641    pub fn check_scope_violation(
642        &self,
643        type_name: &str,
644        scope: DependencyScope,
645    ) -> Option<DependencyScopeError> {
646        // Only check if we're resolving a function-scoped dependency
647        if scope != DependencyScope::Function {
648            return None;
649        }
650
651        let guard = self.stack.read();
652
653        // Find any request-scoped dependency on the stack
654        // (i.e., an outer request-scoped dependency trying to use this function-scoped one)
655        for (_, name, dep_scope) in guard.iter().rev() {
656            if *dep_scope == DependencyScope::Request {
657                return Some(DependencyScopeError::new(
658                    name.clone(),
659                    type_name.to_owned(),
660                ));
661            }
662        }
663
664        None
665    }
666
667    /// Push a type onto the resolution stack with its scope.
668    ///
669    /// Call this when starting to resolve a dependency.
670    pub fn push<T: 'static>(&self, type_name: &str, scope: DependencyScope) {
671        let mut guard = self.stack.write();
672        guard.push((TypeId::of::<T>(), type_name.to_owned(), scope));
673    }
674
675    /// Pop a type from the resolution stack.
676    ///
677    /// Call this when done resolving a dependency (success or error).
678    pub fn pop(&self) {
679        let mut guard = self.stack.write();
680        guard.pop();
681    }
682
683    /// Get the current depth of the resolution stack.
684    #[must_use]
685    pub fn depth(&self) -> usize {
686        let guard = self.stack.read();
687        guard.len()
688    }
689
690    /// Check if the stack is empty.
691    #[must_use]
692    pub fn is_empty(&self) -> bool {
693        self.depth() == 0
694    }
695}
696
697impl Default for ResolutionStack {
698    fn default() -> Self {
699        Self::new()
700    }
701}
702
703impl std::fmt::Debug for ResolutionStack {
704    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705        let guard = self.stack.read();
706        f.debug_struct("ResolutionStack")
707            .field("depth", &guard.len())
708            .field(
709                "types",
710                &guard
711                    .iter()
712                    .map(|(_, name, scope)| format!("{}({:?})", name, scope))
713                    .collect::<Vec<_>>(),
714            )
715            .finish()
716    }
717}
718
719/// RAII guard that automatically pops from the resolution stack on drop.
720///
721/// This ensures the stack is always properly maintained, even if the
722/// resolution fails or panics.
723pub struct ResolutionGuard<'a> {
724    stack: &'a ResolutionStack,
725}
726
727impl<'a> ResolutionGuard<'a> {
728    /// Create a new guard that will pop from the stack when dropped.
729    fn new(stack: &'a ResolutionStack) -> Self {
730        Self { stack }
731    }
732}
733
734impl Drop for ResolutionGuard<'_> {
735    fn drop(&mut self) {
736        self.stack.pop();
737    }
738}
739
740/// Dependency injection extractor.
741#[derive(Debug, Clone)]
742pub struct Depends<T, C = DefaultDependencyConfig>(pub T, PhantomData<C>);
743
744impl<T, C> Depends<T, C> {
745    /// Create a new `Depends` wrapper.
746    #[must_use]
747    pub fn new(value: T) -> Self {
748        Self(value, PhantomData)
749    }
750
751    /// Unwrap the inner value.
752    #[must_use]
753    pub fn into_inner(self) -> T {
754        self.0
755    }
756}
757
758impl<T, C> Deref for Depends<T, C> {
759    type Target = T;
760
761    fn deref(&self) -> &Self::Target {
762        &self.0
763    }
764}
765
766impl<T, C> DerefMut for Depends<T, C> {
767    fn deref_mut(&mut self) -> &mut Self::Target {
768        &mut self.0
769    }
770}
771
772/// Trait for types that can be injected as dependencies.
773pub trait FromDependency: Clone + Send + Sync + 'static {
774    /// Error type when dependency resolution fails.
775    type Error: IntoResponse + Send + Sync + 'static;
776
777    /// Resolve the dependency.
778    fn from_dependency(
779        ctx: &RequestContext,
780        req: &mut Request,
781    ) -> impl Future<Output = Result<Self, Self::Error>> + Send;
782}
783
784impl<T, C> FromRequest for Depends<T, C>
785where
786    T: FromDependency,
787    C: DependsConfig,
788{
789    type Error = T::Error;
790
791    async fn from_request(ctx: &RequestContext, req: &mut Request) -> Result<Self, Self::Error> {
792        // Check cancellation before resolving dependency (overrides or normal)
793        let _ = ctx.checkpoint();
794
795        // Check overrides first (testing support)
796        if let Some(result) = ctx.dependency_overrides().resolve::<T>(ctx, req).await {
797            return result.map(Depends::new);
798        }
799
800        let scope = C::SCOPE.unwrap_or(DependencyScope::Request);
801        let use_cache = C::USE_CACHE && scope == DependencyScope::Request;
802
803        // Check cache - if already resolved, no cycle possible
804        if use_cache {
805            if let Some(cached) = ctx.dependency_cache().get::<T>() {
806                return Ok(Depends::new(cached));
807            }
808        }
809
810        // Check for circular dependency before attempting resolution
811        // (bd-276p: intentional panic for config errors - see module docs)
812        let type_name = std::any::type_name::<T>();
813        if let Some(cycle) = ctx.resolution_stack().check_cycle::<T>(type_name) {
814            handle_circular_dependency(cycle);
815        }
816
817        // Check for scope violation: request-scoped cannot depend on function-scoped
818        if let Some(scope_err) = ctx
819            .resolution_stack()
820            .check_scope_violation(type_name, scope)
821        {
822            handle_scope_violation(scope_err);
823        }
824
825        // Push onto resolution stack and create guard for automatic cleanup
826        ctx.resolution_stack().push::<T>(type_name, scope);
827        let _guard = ResolutionGuard::new(ctx.resolution_stack());
828
829        // Resolve the dependency
830        let _ = ctx.checkpoint();
831        let value = T::from_dependency(ctx, req).await?;
832
833        // Cache if needed (guard will pop stack when dropped)
834        if use_cache {
835            ctx.dependency_cache().insert::<T>(value.clone());
836        }
837
838        Ok(Depends::new(value))
839    }
840}
841
842/// Request-scoped dependency cache.
843pub struct DependencyCache {
844    inner: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
845}
846
847impl DependencyCache {
848    /// Create an empty cache.
849    #[must_use]
850    pub fn new() -> Self {
851        Self {
852            inner: RwLock::new(HashMap::new()),
853        }
854    }
855
856    /// Get a cached dependency by type.
857    #[must_use]
858    pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
859        let guard = self.inner.read();
860        guard
861            .get(&TypeId::of::<T>())
862            .and_then(|boxed| boxed.downcast_ref::<T>())
863            .cloned()
864    }
865
866    /// Insert a dependency into the cache.
867    pub fn insert<T: Clone + Send + Sync + 'static>(&self, value: T) {
868        let mut guard = self.inner.write();
869        guard.insert(TypeId::of::<T>(), Box::new(value));
870    }
871
872    /// Clear all cached dependencies.
873    pub fn clear(&self) {
874        let mut guard = self.inner.write();
875        guard.clear();
876    }
877
878    /// Return the number of cached dependencies.
879    #[must_use]
880    pub fn len(&self) -> usize {
881        let guard = self.inner.read();
882        guard.len()
883    }
884
885    /// Returns true if no dependencies are cached.
886    #[must_use]
887    pub fn is_empty(&self) -> bool {
888        self.len() == 0
889    }
890}
891
892impl Default for DependencyCache {
893    fn default() -> Self {
894        Self::new()
895    }
896}
897
898impl std::fmt::Debug for DependencyCache {
899    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
900        f.debug_struct("DependencyCache")
901            .field("size", &self.len())
902            .finish()
903    }
904}
905
906type OverrideBox = Box<dyn Any + Send + Sync>;
907type OverrideFuture = Pin<Box<dyn Future<Output = Result<OverrideBox, OverrideBox>> + Send>>;
908type OverrideFn = Arc<dyn Fn(&RequestContext, &mut Request) -> OverrideFuture + Send + Sync>;
909
910/// Dependency override registry (primarily for testing).
911pub struct DependencyOverrides {
912    inner: RwLock<HashMap<TypeId, OverrideFn>>,
913}
914
915impl DependencyOverrides {
916    /// Create an empty overrides registry.
917    #[must_use]
918    pub fn new() -> Self {
919        Self {
920            inner: RwLock::new(HashMap::new()),
921        }
922    }
923
924    /// Register an override resolver for a dependency type.
925    pub fn insert<T, F, Fut>(&self, f: F)
926    where
927        T: FromDependency,
928        F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
929        Fut: Future<Output = Result<T, T::Error>> + Send + 'static,
930    {
931        let wrapper: OverrideFn = Arc::new(move |ctx, req| {
932            let fut = f(ctx, req);
933            Box::pin(async move {
934                match fut.await {
935                    Ok(value) => Ok(Box::new(value) as OverrideBox),
936                    Err(err) => Err(Box::new(err) as OverrideBox),
937                }
938            })
939        });
940
941        let mut guard = self.inner.write();
942        guard.insert(TypeId::of::<T>(), wrapper);
943    }
944
945    /// Register a fixed override value for a dependency type.
946    pub fn insert_value<T>(&self, value: T)
947    where
948        T: FromDependency,
949    {
950        self.insert::<T, _, _>(move |_ctx, _req| {
951            let value = value.clone();
952            async move { Ok(value) }
953        });
954    }
955
956    /// Clear all overrides.
957    pub fn clear(&self) {
958        let mut guard = self.inner.write();
959        guard.clear();
960    }
961
962    /// Resolve an override if one exists for `T`.
963    ///
964    /// # Type Safety
965    ///
966    /// The override is looked up by `TypeId::of::<T>()`, which should guarantee
967    /// that the stored closure returns the correct types. If a type mismatch
968    /// occurs despite the TypeId matching:
969    /// - In debug builds: panics via `debug_assert!` to catch bugs early
970    /// - In release builds: returns `None` to fall back to normal resolution
971    pub async fn resolve<T>(
972        &self,
973        ctx: &RequestContext,
974        req: &mut Request,
975    ) -> Option<Result<T, T::Error>>
976    where
977        T: FromDependency,
978    {
979        let override_fn = {
980            let guard = self.inner.read();
981            guard.get(&TypeId::of::<T>()).cloned()
982        };
983
984        let override_fn = override_fn?;
985        match override_fn(ctx, req).await {
986            Ok(value) => match value.downcast::<T>() {
987                Ok(value) => Some(Ok(*value)),
988                Err(_) => {
989                    // Type mismatch should never happen due to TypeId matching.
990                    // Panic in debug builds to catch bugs early.
991                    debug_assert!(
992                        false,
993                        "dependency override type mismatch: expected {}, stored override returned wrong type",
994                        std::any::type_name::<T>()
995                    );
996                    // In release builds, fall back to normal resolution.
997                    None
998                }
999            },
1000            Err(err) => match err.downcast::<T::Error>() {
1001                Ok(err) => Some(Err(*err)),
1002                Err(_) => {
1003                    // Error type mismatch should never happen due to TypeId matching.
1004                    // Panic in debug builds to catch bugs early.
1005                    debug_assert!(
1006                        false,
1007                        "dependency override error type mismatch: expected {}, stored override returned wrong error type",
1008                        std::any::type_name::<T::Error>()
1009                    );
1010                    // In release builds, fall back to normal resolution.
1011                    None
1012                }
1013            },
1014        }
1015    }
1016
1017    /// Return the number of overrides registered.
1018    #[must_use]
1019    pub fn len(&self) -> usize {
1020        let guard = self.inner.read();
1021        guard.len()
1022    }
1023
1024    /// Returns true if no overrides are registered.
1025    #[must_use]
1026    pub fn is_empty(&self) -> bool {
1027        self.len() == 0
1028    }
1029}
1030
1031impl Default for DependencyOverrides {
1032    fn default() -> Self {
1033        Self::new()
1034    }
1035}
1036
1037impl std::fmt::Debug for DependencyOverrides {
1038    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1039        f.debug_struct("DependencyOverrides")
1040            .field("size", &self.len())
1041            .finish()
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048    use crate::error::HttpError;
1049    use crate::request::Method;
1050    use asupersync::Cx;
1051    use std::sync::Arc;
1052    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1053
1054    fn test_context(overrides: Option<Arc<DependencyOverrides>>) -> RequestContext {
1055        let cx = Cx::for_testing();
1056        let request_id = 1;
1057        if let Some(overrides) = overrides {
1058            RequestContext::with_overrides(cx, request_id, overrides)
1059        } else {
1060            RequestContext::new(cx, request_id)
1061        }
1062    }
1063
1064    fn empty_request() -> Request {
1065        Request::new(Method::Get, "/")
1066    }
1067
1068    #[derive(Clone)]
1069    struct CounterDep {
1070        value: usize,
1071    }
1072
1073    impl FromDependency for CounterDep {
1074        type Error = HttpError;
1075
1076        async fn from_dependency(
1077            _ctx: &RequestContext,
1078            _req: &mut Request,
1079        ) -> Result<Self, Self::Error> {
1080            Ok(CounterDep { value: 1 })
1081        }
1082    }
1083
1084    #[test]
1085    fn depends_basic_resolution() {
1086        let ctx = test_context(None);
1087        let mut req = empty_request();
1088        let dep = futures_executor::block_on(Depends::<CounterDep>::from_request(&ctx, &mut req))
1089            .expect("dependency resolution failed");
1090        assert_eq!(dep.value, 1);
1091    }
1092
1093    #[derive(Clone)]
1094    struct CountingDep;
1095
1096    impl FromDependency for CountingDep {
1097        type Error = HttpError;
1098
1099        async fn from_dependency(
1100            ctx: &RequestContext,
1101            _req: &mut Request,
1102        ) -> Result<Self, Self::Error> {
1103            let count = ctx
1104                .dependency_cache()
1105                .get::<Arc<AtomicUsize>>()
1106                .unwrap_or_else(|| Arc::new(AtomicUsize::new(0)));
1107            count.fetch_add(1, Ordering::SeqCst);
1108            ctx.dependency_cache().insert(Arc::clone(&count));
1109            Ok(CountingDep)
1110        }
1111    }
1112
1113    #[test]
1114    fn depends_caches_per_request() {
1115        let ctx = test_context(None);
1116        let mut req = empty_request();
1117
1118        let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
1119            .expect("first resolution failed");
1120        let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
1121            .expect("second resolution failed");
1122
1123        let counter = ctx
1124            .dependency_cache()
1125            .get::<Arc<AtomicUsize>>()
1126            .expect("missing counter");
1127        assert_eq!(counter.load(Ordering::SeqCst), 1);
1128    }
1129
1130    #[test]
1131    fn depends_no_cache_config() {
1132        let ctx = test_context(None);
1133        let mut req = empty_request();
1134
1135        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
1136            &ctx, &mut req,
1137        ))
1138        .expect("first resolution failed");
1139        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
1140            &ctx, &mut req,
1141        ))
1142        .expect("second resolution failed");
1143
1144        let counter = ctx
1145            .dependency_cache()
1146            .get::<Arc<AtomicUsize>>()
1147            .expect("missing counter");
1148        assert_eq!(counter.load(Ordering::SeqCst), 2);
1149    }
1150
1151    #[derive(Clone)]
1152    struct DepB;
1153
1154    impl FromDependency for DepB {
1155        type Error = HttpError;
1156
1157        async fn from_dependency(
1158            _ctx: &RequestContext,
1159            _req: &mut Request,
1160        ) -> Result<Self, Self::Error> {
1161            Ok(DepB)
1162        }
1163    }
1164
1165    #[derive(Clone)]
1166    struct DepA;
1167
1168    impl FromDependency for DepA {
1169        type Error = HttpError;
1170
1171        async fn from_dependency(
1172            ctx: &RequestContext,
1173            req: &mut Request,
1174        ) -> Result<Self, Self::Error> {
1175            let _ = Depends::<DepB>::from_request(ctx, req).await?;
1176            Ok(DepA)
1177        }
1178    }
1179
1180    #[test]
1181    fn depends_nested_resolution() {
1182        let ctx = test_context(None);
1183        let mut req = empty_request();
1184        let _ = futures_executor::block_on(Depends::<DepA>::from_request(&ctx, &mut req))
1185            .expect("nested resolution failed");
1186    }
1187
1188    #[derive(Clone, Debug)]
1189    struct OverrideDep {
1190        value: usize,
1191    }
1192
1193    impl FromDependency for OverrideDep {
1194        type Error = HttpError;
1195
1196        async fn from_dependency(
1197            _ctx: &RequestContext,
1198            _req: &mut Request,
1199        ) -> Result<Self, Self::Error> {
1200            Ok(OverrideDep { value: 1 })
1201        }
1202    }
1203
1204    #[test]
1205    fn depends_override_substitution() {
1206        let overrides = Arc::new(DependencyOverrides::new());
1207        overrides.insert_value(OverrideDep { value: 42 });
1208        let ctx = test_context(Some(overrides));
1209        let mut req = empty_request();
1210
1211        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1212            .expect("override resolution failed");
1213        assert_eq!(dep.value, 42);
1214    }
1215
1216    #[derive(Clone, Debug)]
1217    struct ErrorDep;
1218
1219    impl FromDependency for ErrorDep {
1220        type Error = HttpError;
1221
1222        async fn from_dependency(
1223            _ctx: &RequestContext,
1224            _req: &mut Request,
1225        ) -> Result<Self, Self::Error> {
1226            Err(HttpError::bad_request().with_detail("boom"))
1227        }
1228    }
1229
1230    #[test]
1231    fn depends_error_propagation() {
1232        let ctx = test_context(None);
1233        let mut req = empty_request();
1234        let err = futures_executor::block_on(Depends::<ErrorDep>::from_request(&ctx, &mut req))
1235            .expect_err("expected dependency error");
1236        assert_eq!(err.status.as_u16(), 400);
1237    }
1238
1239    #[derive(Clone)]
1240    struct DepC;
1241
1242    impl FromDependency for DepC {
1243        type Error = HttpError;
1244
1245        async fn from_dependency(
1246            ctx: &RequestContext,
1247            req: &mut Request,
1248        ) -> Result<Self, Self::Error> {
1249            let _ = Depends::<DepA>::from_request(ctx, req).await?;
1250            let _ = Depends::<DepB>::from_request(ctx, req).await?;
1251            Ok(DepC)
1252        }
1253    }
1254
1255    #[test]
1256    fn depends_complex_graph() {
1257        let ctx = test_context(None);
1258        let mut req = empty_request();
1259        let _ = futures_executor::block_on(Depends::<DepC>::from_request(&ctx, &mut req))
1260            .expect("complex graph resolution failed");
1261    }
1262
1263    // ========================================================================
1264    // Circular Dependency Detection Tests
1265    // ========================================================================
1266    //
1267    // NOTE: Testing actual runtime circular dependencies between types is
1268    // difficult because Rust's async trait machinery detects type-level
1269    // cycles at compile time. Instead, we test:
1270    // 1. The ResolutionStack directly (the core mechanism)
1271    // 2. CircularDependencyError formatting
1272    // 3. Diamond patterns (no false positives)
1273    // ========================================================================
1274
1275    // Test: ResolutionStack detects simple A -> B -> A cycle
1276    #[test]
1277    fn resolution_stack_detects_simple_cycle() {
1278        // Create unique marker types for cycle detection
1279        struct TypeA;
1280        struct TypeB;
1281
1282        let stack = ResolutionStack::new();
1283
1284        // Simulate: A starts resolving
1285        // Before pushing, check that TypeA is not on stack
1286        assert!(stack.check_cycle::<TypeA>("TypeA").is_none());
1287        stack.push::<TypeA>("TypeA", DependencyScope::Request);
1288
1289        // Simulate: A resolves B (different type, should not detect cycle)
1290        assert!(stack.check_cycle::<TypeB>("TypeB").is_none());
1291        stack.push::<TypeB>("TypeB", DependencyScope::Request);
1292
1293        // Simulate: B tries to resolve A (cycle!)
1294        let cycle = stack.check_cycle::<TypeA>("TypeA");
1295        assert!(cycle.is_some(), "Should detect A -> B -> A cycle");
1296        let cycle_path = cycle.unwrap();
1297        assert_eq!(cycle_path.len(), 3); // TypeA, TypeB, TypeA
1298        assert_eq!(cycle_path[0], "TypeA");
1299        assert_eq!(cycle_path[1], "TypeB");
1300        assert_eq!(cycle_path[2], "TypeA");
1301    }
1302
1303    // Test: ResolutionStack detects long cycle A -> B -> C -> D -> A
1304    #[test]
1305    fn resolution_stack_detects_long_cycle() {
1306        // Create unique marker types for the test
1307        struct TypeA;
1308        struct TypeB;
1309        struct TypeC;
1310        struct TypeD;
1311
1312        let stack = ResolutionStack::new();
1313
1314        // Simulate: A -> B -> C -> D -> A
1315        stack.push::<TypeA>("TypeA", DependencyScope::Request);
1316        stack.push::<TypeB>("TypeB", DependencyScope::Request);
1317        stack.push::<TypeC>("TypeC", DependencyScope::Request);
1318        stack.push::<TypeD>("TypeD", DependencyScope::Request);
1319
1320        // D tries to resolve A (cycle!)
1321        let cycle = stack.check_cycle::<TypeA>("TypeA");
1322        assert!(cycle.is_some(), "Should detect A -> B -> C -> D -> A cycle");
1323        let cycle_path = cycle.unwrap();
1324        assert_eq!(cycle_path.len(), 5); // TypeA, TypeB, TypeC, TypeD, TypeA
1325        assert_eq!(cycle_path[0], "TypeA");
1326        assert_eq!(cycle_path[4], "TypeA");
1327    }
1328
1329    // Test: ResolutionStack allows diamond pattern (no false positive)
1330    #[test]
1331    fn resolution_stack_allows_diamond() {
1332        struct Top;
1333        struct Left;
1334        struct Right;
1335        struct Bottom;
1336
1337        let stack = ResolutionStack::new();
1338
1339        // Simulate: Top -> Left -> Bottom (complete Left branch)
1340        stack.push::<Top>("Top", DependencyScope::Request);
1341        stack.push::<Left>("Left", DependencyScope::Request);
1342        stack.push::<Bottom>("Bottom", DependencyScope::Request);
1343        stack.pop(); // Bottom done
1344        stack.pop(); // Left done
1345
1346        // Simulate: Top -> Right -> Bottom (Right branch)
1347        stack.push::<Right>("Right", DependencyScope::Request);
1348        // Bottom again - but it's NOT a cycle because we popped it
1349        assert!(
1350            stack.check_cycle::<Bottom>("Bottom").is_none(),
1351            "Diamond pattern should not be detected as a cycle"
1352        );
1353        stack.push::<Bottom>("Bottom", DependencyScope::Request);
1354        stack.pop(); // Bottom done
1355        stack.pop(); // Right done
1356        stack.pop(); // Top done
1357
1358        assert!(stack.is_empty());
1359    }
1360
1361    // Test: ResolutionStack detects self-dependency
1362    #[test]
1363    fn resolution_stack_detects_self_dependency() {
1364        struct SelfRef;
1365
1366        let stack = ResolutionStack::new();
1367        stack.push::<SelfRef>("SelfRef", DependencyScope::Request);
1368
1369        // Try to resolve same type again (self-cycle!)
1370        let cycle = stack.check_cycle::<SelfRef>("SelfRef");
1371        assert!(cycle.is_some(), "Should detect self-dependency");
1372        let cycle_path = cycle.unwrap();
1373        assert_eq!(cycle_path.len(), 2); // SelfRef, SelfRef
1374        assert_eq!(cycle_path[0], "SelfRef");
1375        assert_eq!(cycle_path[1], "SelfRef");
1376    }
1377
1378    // Test: ResolutionStack basic functionality
1379    #[test]
1380    fn resolution_stack_basic() {
1381        let stack = ResolutionStack::new();
1382        assert!(stack.is_empty());
1383        assert_eq!(stack.depth(), 0);
1384
1385        stack.push::<CounterDep>("CounterDep", DependencyScope::Request);
1386        assert!(!stack.is_empty());
1387        assert_eq!(stack.depth(), 1);
1388
1389        // No cycle yet - different type
1390        assert!(stack.check_cycle::<ErrorDep>("ErrorDep").is_none());
1391
1392        // Push another type
1393        stack.push::<ErrorDep>("ErrorDep", DependencyScope::Request);
1394        assert_eq!(stack.depth(), 2);
1395
1396        // Check for cycle with first type - should detect it
1397        let cycle = stack.check_cycle::<CounterDep>("CounterDep");
1398        assert!(cycle.is_some());
1399        let cycle_path = cycle.unwrap();
1400        assert!(cycle_path.contains(&"CounterDep".to_string()));
1401
1402        // Pop and verify
1403        stack.pop();
1404        assert_eq!(stack.depth(), 1);
1405        stack.pop();
1406        assert!(stack.is_empty());
1407    }
1408
1409    // Test: CircularDependencyError formatting
1410    #[test]
1411    fn circular_dependency_error_formatting() {
1412        let err = CircularDependencyError::new(vec![
1413            "DbPool".to_string(),
1414            "UserService".to_string(),
1415            "AuthService".to_string(),
1416            "DbPool".to_string(),
1417        ]);
1418        let msg = err.to_string();
1419        assert!(msg.contains("Circular dependency detected"));
1420        assert!(msg.contains("DbPool -> UserService -> AuthService -> DbPool"));
1421        assert_eq!(
1422            err.cycle_path(),
1423            "DbPool -> UserService -> AuthService -> DbPool"
1424        );
1425    }
1426
1427    // Test: CircularDependencyError into_response
1428    #[test]
1429    fn circular_dependency_error_into_response() {
1430        let err =
1431            CircularDependencyError::new(vec!["A".to_string(), "B".to_string(), "A".to_string()]);
1432        let response = err.into_response();
1433        assert_eq!(response.status().as_u16(), 500);
1434    }
1435
1436    // Test: Diamond dependency graph resolves correctly (runtime test)
1437    #[derive(Clone)]
1438    struct DiamondBottom {
1439        id: u32,
1440    }
1441    #[derive(Clone)]
1442    struct DiamondLeft {
1443        bottom_id: u32,
1444    }
1445    #[derive(Clone)]
1446    struct DiamondRight {
1447        bottom_id: u32,
1448    }
1449    #[derive(Clone)]
1450    struct DiamondTop {
1451        left_id: u32,
1452        right_id: u32,
1453    }
1454
1455    impl FromDependency for DiamondBottom {
1456        type Error = HttpError;
1457        async fn from_dependency(
1458            _ctx: &RequestContext,
1459            _req: &mut Request,
1460        ) -> Result<Self, Self::Error> {
1461            Ok(DiamondBottom { id: 42 })
1462        }
1463    }
1464
1465    impl FromDependency for DiamondLeft {
1466        type Error = HttpError;
1467        async fn from_dependency(
1468            ctx: &RequestContext,
1469            req: &mut Request,
1470        ) -> Result<Self, Self::Error> {
1471            let bottom = Depends::<DiamondBottom>::from_request(ctx, req).await?;
1472            Ok(DiamondLeft {
1473                bottom_id: bottom.id,
1474            })
1475        }
1476    }
1477
1478    impl FromDependency for DiamondRight {
1479        type Error = HttpError;
1480        async fn from_dependency(
1481            ctx: &RequestContext,
1482            req: &mut Request,
1483        ) -> Result<Self, Self::Error> {
1484            let bottom = Depends::<DiamondBottom>::from_request(ctx, req).await?;
1485            Ok(DiamondRight {
1486                bottom_id: bottom.id,
1487            })
1488        }
1489    }
1490
1491    impl FromDependency for DiamondTop {
1492        type Error = HttpError;
1493        async fn from_dependency(
1494            ctx: &RequestContext,
1495            req: &mut Request,
1496        ) -> Result<Self, Self::Error> {
1497            let left = Depends::<DiamondLeft>::from_request(ctx, req).await?;
1498            let right = Depends::<DiamondRight>::from_request(ctx, req).await?;
1499            Ok(DiamondTop {
1500                left_id: left.bottom_id,
1501                right_id: right.bottom_id,
1502            })
1503        }
1504    }
1505
1506    #[test]
1507    fn diamond_pattern_resolves_correctly() {
1508        let ctx = test_context(None);
1509        let mut req = empty_request();
1510        // Diamond pattern should NOT trigger circular dependency detection
1511        let result =
1512            futures_executor::block_on(Depends::<DiamondTop>::from_request(&ctx, &mut req));
1513        assert!(result.is_ok(), "Diamond pattern should not be a cycle");
1514        let top = result.unwrap();
1515        assert_eq!(top.left_id, 42);
1516        assert_eq!(top.right_id, 42);
1517    }
1518
1519    // ========================================================================
1520    // Additional DI Tests for Coverage (fastapi_rust-zf4)
1521    // ========================================================================
1522
1523    // Test: Override affects nested dependencies
1524    #[derive(Clone)]
1525    struct NestedInnerDep {
1526        value: String,
1527    }
1528
1529    impl FromDependency for NestedInnerDep {
1530        type Error = HttpError;
1531        async fn from_dependency(
1532            _ctx: &RequestContext,
1533            _req: &mut Request,
1534        ) -> Result<Self, Self::Error> {
1535            Ok(NestedInnerDep {
1536                value: "original".to_string(),
1537            })
1538        }
1539    }
1540
1541    #[derive(Clone)]
1542    struct NestedOuterDep {
1543        inner_value: String,
1544    }
1545
1546    impl FromDependency for NestedOuterDep {
1547        type Error = HttpError;
1548        async fn from_dependency(
1549            ctx: &RequestContext,
1550            req: &mut Request,
1551        ) -> Result<Self, Self::Error> {
1552            let inner = Depends::<NestedInnerDep>::from_request(ctx, req).await?;
1553            Ok(NestedOuterDep {
1554                inner_value: inner.value.clone(),
1555            })
1556        }
1557    }
1558
1559    #[test]
1560    fn override_affects_nested_dependencies() {
1561        let overrides = Arc::new(DependencyOverrides::new());
1562        overrides.insert_value(NestedInnerDep {
1563            value: "overridden".to_string(),
1564        });
1565        let ctx = test_context(Some(overrides));
1566        let mut req = empty_request();
1567
1568        let result =
1569            futures_executor::block_on(Depends::<NestedOuterDep>::from_request(&ctx, &mut req))
1570                .expect("nested override resolution failed");
1571
1572        assert_eq!(
1573            result.inner_value, "overridden",
1574            "Override should propagate to nested dependencies"
1575        );
1576    }
1577
1578    // Test: Clear overrides works
1579    #[test]
1580    fn clear_overrides_restores_original() {
1581        let overrides = Arc::new(DependencyOverrides::new());
1582        overrides.insert_value(OverrideDep { value: 99 });
1583        assert_eq!(overrides.len(), 1);
1584
1585        overrides.clear();
1586        assert_eq!(overrides.len(), 0);
1587        assert!(overrides.is_empty());
1588
1589        // After clear, should return None (no override)
1590        let ctx = test_context(Some(overrides));
1591        let mut req = empty_request();
1592        let result =
1593            futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1594                .expect("resolution after clear failed");
1595
1596        // Should get original value (1) not overridden value (99)
1597        assert_eq!(result.value, 1, "After clear, should resolve to original");
1598    }
1599
1600    // Test: Multiple dependencies in handler simulation
1601    #[derive(Clone)]
1602    struct DepX {
1603        x: i32,
1604    }
1605    #[derive(Clone)]
1606    struct DepY {
1607        y: i32,
1608    }
1609    #[derive(Clone)]
1610    struct DepZ {
1611        z: i32,
1612    }
1613
1614    impl FromDependency for DepX {
1615        type Error = HttpError;
1616        async fn from_dependency(
1617            _ctx: &RequestContext,
1618            _req: &mut Request,
1619        ) -> Result<Self, Self::Error> {
1620            Ok(DepX { x: 10 })
1621        }
1622    }
1623
1624    impl FromDependency for DepY {
1625        type Error = HttpError;
1626        async fn from_dependency(
1627            _ctx: &RequestContext,
1628            _req: &mut Request,
1629        ) -> Result<Self, Self::Error> {
1630            Ok(DepY { y: 20 })
1631        }
1632    }
1633
1634    impl FromDependency for DepZ {
1635        type Error = HttpError;
1636        async fn from_dependency(
1637            _ctx: &RequestContext,
1638            _req: &mut Request,
1639        ) -> Result<Self, Self::Error> {
1640            Ok(DepZ { z: 30 })
1641        }
1642    }
1643
1644    #[test]
1645    fn multiple_independent_dependencies() {
1646        let ctx = test_context(None);
1647        let mut req = empty_request();
1648
1649        // Simulate a handler that needs X, Y, and Z
1650        let dep_x =
1651            futures_executor::block_on(Depends::<DepX>::from_request(&ctx, &mut req)).unwrap();
1652        let dep_y =
1653            futures_executor::block_on(Depends::<DepY>::from_request(&ctx, &mut req)).unwrap();
1654        let dep_z =
1655            futures_executor::block_on(Depends::<DepZ>::from_request(&ctx, &mut req)).unwrap();
1656
1657        assert_eq!(dep_x.x, 10);
1658        assert_eq!(dep_y.y, 20);
1659        assert_eq!(dep_z.z, 30);
1660    }
1661
1662    // Test: Request scope isolation (different contexts have independent caches)
1663    #[test]
1664    fn request_scope_isolation() {
1665        // Request 1
1666        let ctx1 = test_context(None);
1667        let mut req1 = empty_request();
1668
1669        let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx1, &mut req1))
1670            .unwrap();
1671        let counter1 = ctx1.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
1672
1673        // Request 2 - fresh context, fresh cache
1674        let ctx2 = test_context(None);
1675        let mut req2 = empty_request();
1676
1677        let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx2, &mut req2))
1678            .unwrap();
1679        let counter2 = ctx2.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
1680
1681        // Each request should have its own counter
1682        assert_eq!(counter1.load(Ordering::SeqCst), 1);
1683        assert_eq!(counter2.load(Ordering::SeqCst), 1);
1684
1685        // They should be different Arc instances
1686        assert!(!Arc::ptr_eq(&counter1, &counter2));
1687    }
1688
1689    // Test: Function scope (NoCache) creates fresh instance each time
1690    #[test]
1691    fn function_scope_no_caching() {
1692        let ctx = test_context(None);
1693        let mut req = empty_request();
1694
1695        // First call with NoCache
1696        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
1697            &ctx, &mut req,
1698        ))
1699        .unwrap();
1700
1701        // Second call with NoCache - should create new instance
1702        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
1703            &ctx, &mut req,
1704        ))
1705        .unwrap();
1706
1707        // Third call with NoCache
1708        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
1709            &ctx, &mut req,
1710        ))
1711        .unwrap();
1712
1713        let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
1714        assert_eq!(
1715            counter.load(Ordering::SeqCst),
1716            3,
1717            "NoCache should resolve 3 times"
1718        );
1719    }
1720
1721    // Test: Async dependency execution (verifies async works correctly)
1722    #[derive(Clone)]
1723    struct AsyncDep {
1724        computed: u64,
1725    }
1726
1727    impl FromDependency for AsyncDep {
1728        type Error = HttpError;
1729        async fn from_dependency(
1730            _ctx: &RequestContext,
1731            _req: &mut Request,
1732        ) -> Result<Self, Self::Error> {
1733            // Simulate some async computation
1734            let result = async {
1735                let a = 21u64;
1736                let b = 21u64;
1737                a + b
1738            }
1739            .await;
1740            Ok(AsyncDep { computed: result })
1741        }
1742    }
1743
1744    #[test]
1745    fn async_dependency_resolution() {
1746        let ctx = test_context(None);
1747        let mut req = empty_request();
1748
1749        let dep = futures_executor::block_on(Depends::<AsyncDep>::from_request(&ctx, &mut req))
1750            .expect("async dependency resolution failed");
1751
1752        assert_eq!(dep.computed, 42);
1753    }
1754
1755    // Test: Error in nested dependency propagates correctly
1756    #[derive(Clone, Debug)]
1757    struct DepThatDependsOnError;
1758
1759    impl FromDependency for DepThatDependsOnError {
1760        type Error = HttpError;
1761        async fn from_dependency(
1762            ctx: &RequestContext,
1763            req: &mut Request,
1764        ) -> Result<Self, Self::Error> {
1765            // This will fail because ErrorDep always returns an error
1766            let _ = Depends::<ErrorDep>::from_request(ctx, req).await?;
1767            Ok(DepThatDependsOnError)
1768        }
1769    }
1770
1771    #[test]
1772    fn nested_error_propagation() {
1773        let ctx = test_context(None);
1774        let mut req = empty_request();
1775
1776        let result = futures_executor::block_on(Depends::<DepThatDependsOnError>::from_request(
1777            &ctx, &mut req,
1778        ));
1779
1780        assert!(result.is_err(), "Nested error should propagate");
1781        let err = result.unwrap_err();
1782        assert_eq!(err.status.as_u16(), 400);
1783    }
1784
1785    // Test: DependencyCache clear and len methods
1786    #[test]
1787    fn dependency_cache_operations() {
1788        let cache = DependencyCache::new();
1789        assert!(cache.is_empty());
1790        assert_eq!(cache.len(), 0);
1791
1792        cache.insert::<String>("test".to_string());
1793        assert!(!cache.is_empty());
1794        assert_eq!(cache.len(), 1);
1795
1796        cache.insert::<i32>(42);
1797        assert_eq!(cache.len(), 2);
1798
1799        let retrieved = cache.get::<String>().unwrap();
1800        assert_eq!(retrieved, "test");
1801
1802        cache.clear();
1803        assert!(cache.is_empty());
1804        assert!(cache.get::<String>().is_none());
1805    }
1806
1807    // Test: DependencyOverrides dynamic resolver
1808    #[test]
1809    fn dynamic_override_resolver() {
1810        let overrides = Arc::new(DependencyOverrides::new());
1811
1812        // Register a dynamic resolver that computes value based on context
1813        overrides.insert::<OverrideDep, _, _>(|_ctx, _req| async move {
1814            // Could use ctx/req to compute different values
1815            Ok(OverrideDep { value: 100 })
1816        });
1817
1818        let ctx = test_context(Some(overrides));
1819        let mut req = empty_request();
1820
1821        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1822            .expect("dynamic override failed");
1823
1824        assert_eq!(dep.value, 100);
1825    }
1826
1827    // ---- Comprehensive DependencyOverrides tests (bd-3pbd) ----
1828
1829    #[test]
1830    fn overrides_new_is_empty() {
1831        let overrides = DependencyOverrides::new();
1832        assert!(overrides.is_empty());
1833        assert_eq!(overrides.len(), 0);
1834    }
1835
1836    #[test]
1837    fn overrides_default_is_empty() {
1838        let overrides = DependencyOverrides::default();
1839        assert!(overrides.is_empty());
1840        assert_eq!(overrides.len(), 0);
1841    }
1842
1843    #[test]
1844    fn overrides_debug_format() {
1845        let overrides = DependencyOverrides::new();
1846        let debug = format!("{:?}", overrides);
1847        assert!(debug.contains("DependencyOverrides"));
1848        assert!(debug.contains("size"));
1849    }
1850
1851    #[test]
1852    fn overrides_insert_value_increments_len() {
1853        let overrides = DependencyOverrides::new();
1854        assert_eq!(overrides.len(), 0);
1855
1856        overrides.insert_value(OverrideDep { value: 42 });
1857        assert_eq!(overrides.len(), 1);
1858        assert!(!overrides.is_empty());
1859    }
1860
1861    #[test]
1862    fn overrides_multiple_types_registered() {
1863        let overrides = Arc::new(DependencyOverrides::new());
1864        overrides.insert_value(OverrideDep { value: 10 });
1865        overrides.insert_value(NestedInnerDep {
1866            value: "mocked".to_string(),
1867        });
1868        assert_eq!(overrides.len(), 2);
1869
1870        // Resolve each type independently
1871        let ctx = test_context(Some(overrides.clone()));
1872        let mut req = empty_request();
1873
1874        let dep1 = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1875            .expect("OverrideDep override failed");
1876        assert_eq!(dep1.value, 10);
1877
1878        let mut req2 = empty_request();
1879        let dep2 =
1880            futures_executor::block_on(Depends::<NestedInnerDep>::from_request(&ctx, &mut req2))
1881                .expect("NestedInnerDep override failed");
1882        assert_eq!(dep2.value, "mocked");
1883    }
1884
1885    #[test]
1886    fn overrides_replace_same_type() {
1887        let overrides = Arc::new(DependencyOverrides::new());
1888        overrides.insert_value(OverrideDep { value: 1 });
1889        assert_eq!(overrides.len(), 1);
1890
1891        // Replace with a different value
1892        overrides.insert_value(OverrideDep { value: 999 });
1893        assert_eq!(overrides.len(), 1); // Still 1, replaced
1894
1895        let ctx = test_context(Some(overrides));
1896        let mut req = empty_request();
1897
1898        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1899            .expect("override resolution failed");
1900        assert_eq!(dep.value, 999);
1901    }
1902
1903    #[test]
1904    fn overrides_clear_removes_all() {
1905        let overrides = DependencyOverrides::new();
1906        overrides.insert_value(OverrideDep { value: 42 });
1907        overrides.insert_value(NestedInnerDep {
1908            value: "mock".to_string(),
1909        });
1910        assert_eq!(overrides.len(), 2);
1911
1912        overrides.clear();
1913        assert!(overrides.is_empty());
1914        assert_eq!(overrides.len(), 0);
1915    }
1916
1917    #[test]
1918    fn overrides_resolve_returns_none_for_unregistered_type() {
1919        let overrides = Arc::new(DependencyOverrides::new());
1920        // Only register OverrideDep
1921        overrides.insert_value(OverrideDep { value: 42 });
1922
1923        let ctx = test_context(Some(overrides.clone()));
1924        let mut req = empty_request();
1925
1926        // NestedInnerDep is NOT overridden, resolve should return None
1927        let result =
1928            futures_executor::block_on(overrides.resolve::<NestedInnerDep>(&ctx, &mut req));
1929        assert!(result.is_none(), "Unregistered type should resolve to None");
1930    }
1931
1932    #[test]
1933    fn overrides_resolve_some_for_registered_type() {
1934        let overrides = Arc::new(DependencyOverrides::new());
1935        overrides.insert_value(OverrideDep { value: 77 });
1936
1937        let ctx = test_context(Some(overrides.clone()));
1938        let mut req = empty_request();
1939
1940        let result = futures_executor::block_on(overrides.resolve::<OverrideDep>(&ctx, &mut req));
1941        assert!(result.is_some());
1942        let dep = result.unwrap().expect("resolve should succeed");
1943        assert_eq!(dep.value, 77);
1944    }
1945
1946    #[test]
1947    fn overrides_not_affect_unrelated_dependency() {
1948        let overrides = Arc::new(DependencyOverrides::new());
1949        // Override only NestedInnerDep
1950        overrides.insert_value(NestedInnerDep {
1951            value: "overridden".to_string(),
1952        });
1953
1954        let ctx = test_context(Some(overrides));
1955        let mut req = empty_request();
1956
1957        // OverrideDep should still use its real implementation (returns value: 1)
1958        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1959            .expect("should resolve from real implementation");
1960        assert_eq!(
1961            dep.value, 1,
1962            "Unoverridden dep should use real implementation"
1963        );
1964    }
1965
1966    #[test]
1967    fn overrides_take_precedence_over_cache() {
1968        let overrides = Arc::new(DependencyOverrides::new());
1969        overrides.insert_value(OverrideDep { value: 42 });
1970        let ctx = test_context(Some(overrides));
1971
1972        // Pre-populate cache with a different value
1973        ctx.dependency_cache().insert(OverrideDep { value: 999 });
1974
1975        let mut req = empty_request();
1976        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1977            .expect("override should take precedence");
1978
1979        // Override should win over cache
1980        assert_eq!(dep.value, 42, "Override should take precedence over cache");
1981    }
1982
1983    #[test]
1984    fn overrides_dynamic_resolver_can_return_error() {
1985        let overrides = Arc::new(DependencyOverrides::new());
1986
1987        overrides.insert::<OverrideDep, _, _>(|_ctx, _req| async move {
1988            Err(
1989                HttpError::new(crate::response::StatusCode::INTERNAL_SERVER_ERROR)
1990                    .with_detail("override error"),
1991            )
1992        });
1993
1994        let ctx = test_context(Some(overrides));
1995        let mut req = empty_request();
1996
1997        let err = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
1998            .expect_err("override should return error");
1999        assert_eq!(err.status.as_u16(), 500);
2000    }
2001
2002    #[test]
2003    fn overrides_insert_value_works_for_multiple_resolves() {
2004        // insert_value uses Clone, so the value should work for multiple resolves
2005        let overrides = Arc::new(DependencyOverrides::new());
2006        overrides.insert_value(OverrideDep { value: 7 });
2007
2008        let ctx = test_context(Some(overrides));
2009
2010        for _ in 0..5 {
2011            let mut req = empty_request();
2012            let dep =
2013                futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
2014                    .expect("repeated resolve should work");
2015            assert_eq!(dep.value, 7);
2016        }
2017    }
2018
2019    #[test]
2020    fn overrides_dynamic_resolver_accesses_request() {
2021        let overrides = Arc::new(DependencyOverrides::new());
2022
2023        // Dynamic resolver that reads from request extensions
2024        overrides.insert::<OverrideDep, _, _>(|_ctx, req| {
2025            let value = req.get_extension::<usize>().copied().unwrap_or(0);
2026            async move { Ok(OverrideDep { value }) }
2027        });
2028
2029        let ctx = test_context(Some(overrides));
2030        let mut req = empty_request();
2031        req.insert_extension(42usize);
2032
2033        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
2034            .expect("dynamic resolver with request access failed");
2035        assert_eq!(dep.value, 42, "Dynamic resolver should read from request");
2036    }
2037
2038    #[test]
2039    fn overrides_after_clear_fall_back_to_real_dependency() {
2040        let overrides = Arc::new(DependencyOverrides::new());
2041        overrides.insert_value(OverrideDep { value: 999 });
2042
2043        // Verify override works
2044        let ctx = test_context(Some(overrides.clone()));
2045        let mut req = empty_request();
2046        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
2047            .unwrap();
2048        assert_eq!(dep.value, 999);
2049
2050        // Clear and verify fallback to real dependency
2051        overrides.clear();
2052        let ctx2 = test_context(Some(overrides));
2053        let mut req2 = empty_request();
2054        let dep2 =
2055            futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx2, &mut req2))
2056                .unwrap();
2057        assert_eq!(dep2.value, 1, "After clear, real dependency should be used");
2058    }
2059
2060    #[test]
2061    fn overrides_without_overrides_use_real_dependency() {
2062        // No overrides at all
2063        let ctx = test_context(None);
2064        let mut req = empty_request();
2065
2066        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
2067            .unwrap();
2068        assert_eq!(
2069            dep.value, 1,
2070            "Without overrides, real dependency should be used"
2071        );
2072    }
2073
2074    // Test: ResolutionGuard properly cleans up on drop
2075    #[test]
2076    fn resolution_guard_cleanup() {
2077        let stack = ResolutionStack::new();
2078        stack.push::<CounterDep>("CounterDep", DependencyScope::Request);
2079        assert_eq!(stack.depth(), 1);
2080
2081        {
2082            // Create guard within a scope
2083            let _guard = ResolutionGuard::new(&stack);
2084            stack.push::<ErrorDep>("ErrorDep", DependencyScope::Request);
2085            assert_eq!(stack.depth(), 2);
2086            // _guard will pop when dropped
2087        }
2088
2089        // After guard drops, one item should be popped
2090        // Note: guard pops, but we still have CounterDep
2091        assert_eq!(stack.depth(), 1);
2092    }
2093
2094    // ========================================================================
2095    // CleanupStack Tests (fastapi_rust-9ps)
2096    // ========================================================================
2097
2098    #[test]
2099    fn cleanup_stack_basic() {
2100        let stack = CleanupStack::new();
2101        assert!(stack.is_empty());
2102        assert_eq!(stack.len(), 0);
2103
2104        // Register a cleanup
2105        let counter = Arc::new(AtomicUsize::new(0));
2106        let counter_clone = Arc::clone(&counter);
2107        stack.push(Box::new(move || {
2108            Box::pin(async move {
2109                counter_clone.fetch_add(1, Ordering::SeqCst);
2110            }) as Pin<Box<dyn Future<Output = ()> + Send>>
2111        }));
2112
2113        assert!(!stack.is_empty());
2114        assert_eq!(stack.len(), 1);
2115
2116        // Run cleanups
2117        let completed = futures_executor::block_on(stack.run_cleanups());
2118        assert_eq!(completed, 1);
2119        assert_eq!(counter.load(Ordering::SeqCst), 1);
2120
2121        // Stack should be empty after running
2122        assert!(stack.is_empty());
2123    }
2124
2125    #[test]
2126    fn cleanup_stack_lifo_order() {
2127        // Track cleanup execution order
2128        let order = Arc::new(parking_lot::Mutex::new(Vec::<i32>::new()));
2129
2130        let stack = CleanupStack::new();
2131
2132        // Register cleanups: 1, 2, 3
2133        for i in 1..=3 {
2134            let order_clone = Arc::clone(&order);
2135            stack.push(Box::new(move || {
2136                Box::pin(async move {
2137                    order_clone.lock().push(i);
2138                }) as Pin<Box<dyn Future<Output = ()> + Send>>
2139            }));
2140        }
2141
2142        // Run cleanups - should execute in LIFO order: 3, 2, 1
2143        futures_executor::block_on(stack.run_cleanups());
2144
2145        let executed_order = order.lock().clone();
2146        assert_eq!(
2147            executed_order,
2148            vec![3, 2, 1],
2149            "Cleanups should run in LIFO order"
2150        );
2151    }
2152
2153    #[test]
2154    fn cleanup_stack_take_cleanups() {
2155        let stack = CleanupStack::new();
2156
2157        // Register 3 cleanups
2158        for _ in 0..3 {
2159            stack.push(Box::new(|| {
2160                Box::pin(async {}) as Pin<Box<dyn Future<Output = ()> + Send>>
2161            }));
2162        }
2163
2164        assert_eq!(stack.len(), 3);
2165
2166        // Take cleanups
2167        let cleanups = stack.take_cleanups();
2168        assert_eq!(cleanups.len(), 3);
2169
2170        // Stack should be empty
2171        assert!(stack.is_empty());
2172    }
2173
2174    #[test]
2175    fn cleanup_stack_multiple_runs() {
2176        let counter = Arc::new(AtomicUsize::new(0));
2177        let stack = CleanupStack::new();
2178
2179        // First batch
2180        let counter_clone = Arc::clone(&counter);
2181        stack.push(Box::new(move || {
2182            Box::pin(async move {
2183                counter_clone.fetch_add(1, Ordering::SeqCst);
2184            }) as Pin<Box<dyn Future<Output = ()> + Send>>
2185        }));
2186        futures_executor::block_on(stack.run_cleanups());
2187
2188        // Second batch
2189        let counter_clone = Arc::clone(&counter);
2190        stack.push(Box::new(move || {
2191            Box::pin(async move {
2192                counter_clone.fetch_add(10, Ordering::SeqCst);
2193            }) as Pin<Box<dyn Future<Output = ()> + Send>>
2194        }));
2195        futures_executor::block_on(stack.run_cleanups());
2196
2197        assert_eq!(counter.load(Ordering::SeqCst), 11);
2198    }
2199
2200    #[test]
2201    fn cleanup_stack_panic_continues() {
2202        // Test that if one cleanup panics, remaining cleanups still run (bd-35r4)
2203        let order = Arc::new(parking_lot::Mutex::new(Vec::<i32>::new()));
2204
2205        let stack = CleanupStack::new();
2206
2207        // First cleanup: runs normally
2208        let order_clone = Arc::clone(&order);
2209        stack.push(Box::new(move || {
2210            Box::pin(async move {
2211                order_clone.lock().push(1);
2212            }) as Pin<Box<dyn Future<Output = ()> + Send>>
2213        }));
2214
2215        // Second cleanup: panics during creation
2216        stack.push(Box::new(|| -> Pin<Box<dyn Future<Output = ()> + Send>> {
2217            panic!("cleanup 2 panics");
2218        }));
2219
2220        // Third cleanup: runs normally
2221        let order_clone = Arc::clone(&order);
2222        stack.push(Box::new(move || {
2223            Box::pin(async move {
2224                order_clone.lock().push(3);
2225            }) as Pin<Box<dyn Future<Output = ()> + Send>>
2226        }));
2227
2228        // Run cleanups - LIFO order: 3, 2 (panics), 1
2229        let completed = futures_executor::block_on(stack.run_cleanups());
2230
2231        // Only 2 should complete (cleanup 2 panicked)
2232        assert_eq!(completed, 2, "Should report 2 successful cleanups");
2233
2234        let executed_order = order.lock().clone();
2235        // Should still run cleanup 3 and 1 despite cleanup 2 panicking
2236        assert_eq!(
2237            executed_order,
2238            vec![3, 1],
2239            "Cleanups should continue after panic"
2240        );
2241    }
2242
2243    #[test]
2244    fn cleanup_runs_after_handler_error() {
2245        // Test that cleanups run even when handler returns an error (bd-35r4)
2246        // This simulates the server calling run_cleanups after any handler result
2247
2248        let cleanup_ran = Arc::new(AtomicBool::new(false));
2249        let cleanup_ran_clone = Arc::clone(&cleanup_ran);
2250
2251        let ctx = test_context(None);
2252
2253        // Register a cleanup
2254        ctx.cleanup_stack().push(Box::new(move || {
2255            Box::pin(async move {
2256                cleanup_ran_clone.store(true, Ordering::SeqCst);
2257            }) as Pin<Box<dyn Future<Output = ()> + Send>>
2258        }));
2259
2260        // Simulate handler returning an error (we just don't use the result)
2261        let handler_result: Result<(), HttpError> =
2262            Err(HttpError::new(StatusCode::INTERNAL_SERVER_ERROR).with_detail("handler failed"));
2263
2264        // Server always runs cleanups after handler, regardless of result
2265        futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
2266
2267        assert!(
2268            cleanup_ran.load(Ordering::SeqCst),
2269            "Cleanup should run even after handler error"
2270        );
2271
2272        // The handler error is still propagated
2273        assert!(handler_result.is_err());
2274    }
2275
2276    // ========================================================================
2277    // DependsCleanup Tests (fastapi_rust-9ps)
2278    // ========================================================================
2279
2280    /// A dependency that tracks setup and cleanup
2281    #[derive(Clone)]
2282    struct TrackedResource {
2283        id: u32,
2284    }
2285
2286    impl FromDependencyWithCleanup for TrackedResource {
2287        type Value = TrackedResource;
2288        type Error = HttpError;
2289
2290        async fn setup(
2291            ctx: &RequestContext,
2292            _req: &mut Request,
2293        ) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
2294            // Get or create a tracker in the dependency cache
2295            let tracker = ctx
2296                .dependency_cache()
2297                .get::<Arc<parking_lot::Mutex<Vec<String>>>>()
2298                .unwrap_or_else(|| {
2299                    let t = Arc::new(parking_lot::Mutex::new(Vec::new()));
2300                    ctx.dependency_cache().insert(Arc::clone(&t));
2301                    t
2302                });
2303
2304            tracker.lock().push("setup:resource".to_string());
2305
2306            let cleanup_tracker = Arc::clone(&tracker);
2307            let cleanup = Box::new(move || {
2308                Box::pin(async move {
2309                    cleanup_tracker.lock().push("cleanup:resource".to_string());
2310                }) as Pin<Box<dyn Future<Output = ()> + Send>>
2311            }) as CleanupFn;
2312
2313            Ok((TrackedResource { id: 42 }, Some(cleanup)))
2314        }
2315    }
2316
2317    #[test]
2318    fn depends_cleanup_registers_cleanup() {
2319        let ctx = test_context(None);
2320        let mut req = empty_request();
2321
2322        // Resolve the dependency
2323        let dep = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
2324            &ctx, &mut req,
2325        ))
2326        .expect("cleanup dependency resolution failed");
2327
2328        assert_eq!(dep.id, 42);
2329
2330        // Cleanup should be registered
2331        assert_eq!(ctx.cleanup_stack().len(), 1);
2332
2333        // Get tracker
2334        let tracker = ctx
2335            .dependency_cache()
2336            .get::<Arc<parking_lot::Mutex<Vec<String>>>>()
2337            .unwrap();
2338
2339        // Only setup should have run
2340        let events = tracker.lock().clone();
2341        assert_eq!(events, vec!["setup:resource"]);
2342
2343        // Run cleanups
2344        futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
2345
2346        // Now cleanup should have run too
2347        let events = tracker.lock().clone();
2348        assert_eq!(events, vec!["setup:resource", "cleanup:resource"]);
2349    }
2350
2351    /// A dependency without cleanup
2352    #[derive(Clone)]
2353    struct NoCleanupResource {
2354        value: String,
2355    }
2356
2357    impl FromDependencyWithCleanup for NoCleanupResource {
2358        type Value = NoCleanupResource;
2359        type Error = HttpError;
2360
2361        async fn setup(
2362            _ctx: &RequestContext,
2363            _req: &mut Request,
2364        ) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
2365            Ok((
2366                NoCleanupResource {
2367                    value: "no cleanup".to_string(),
2368                },
2369                None, // No cleanup needed
2370            ))
2371        }
2372    }
2373
2374    #[test]
2375    fn depends_cleanup_no_cleanup_fn() {
2376        let ctx = test_context(None);
2377        let mut req = empty_request();
2378
2379        let dep = futures_executor::block_on(DependsCleanup::<NoCleanupResource>::from_request(
2380            &ctx, &mut req,
2381        ))
2382        .expect("no cleanup dependency resolution failed");
2383
2384        assert_eq!(dep.value, "no cleanup");
2385
2386        // No cleanup should be registered
2387        assert!(ctx.cleanup_stack().is_empty());
2388    }
2389
2390    /// Nested dependencies with cleanup
2391    #[derive(Clone)]
2392    struct OuterWithCleanup {
2393        inner_id: u32,
2394    }
2395
2396    impl FromDependencyWithCleanup for OuterWithCleanup {
2397        type Value = OuterWithCleanup;
2398        type Error = HttpError;
2399
2400        async fn setup(
2401            ctx: &RequestContext,
2402            req: &mut Request,
2403        ) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
2404            // Resolve inner dependency first
2405            let inner = DependsCleanup::<TrackedResource>::from_request(ctx, req).await?;
2406
2407            // Get tracker
2408            let tracker = ctx
2409                .dependency_cache()
2410                .get::<Arc<parking_lot::Mutex<Vec<String>>>>()
2411                .unwrap();
2412
2413            tracker.lock().push("setup:outer".to_string());
2414
2415            let cleanup_tracker = Arc::clone(&tracker);
2416            let cleanup = Box::new(move || {
2417                Box::pin(async move {
2418                    cleanup_tracker.lock().push("cleanup:outer".to_string());
2419                }) as Pin<Box<dyn Future<Output = ()> + Send>>
2420            }) as CleanupFn;
2421
2422            Ok((OuterWithCleanup { inner_id: inner.id }, Some(cleanup)))
2423        }
2424    }
2425
2426    #[test]
2427    fn depends_cleanup_nested_lifo() {
2428        let ctx = test_context(None);
2429        let mut req = empty_request();
2430
2431        // Resolve outer (which resolves inner)
2432        let dep = futures_executor::block_on(DependsCleanup::<OuterWithCleanup>::from_request(
2433            &ctx, &mut req,
2434        ))
2435        .expect("nested cleanup dependency resolution failed");
2436
2437        assert_eq!(dep.inner_id, 42);
2438
2439        // Both cleanups should be registered
2440        assert_eq!(ctx.cleanup_stack().len(), 2);
2441
2442        // Get tracker
2443        let tracker = ctx
2444            .dependency_cache()
2445            .get::<Arc<parking_lot::Mutex<Vec<String>>>>()
2446            .unwrap();
2447
2448        // Setup order: inner, then outer
2449        let events_before = tracker.lock().clone();
2450        assert_eq!(events_before, vec!["setup:resource", "setup:outer"]);
2451
2452        // Run cleanups - LIFO order: outer first, then inner
2453        futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
2454
2455        let events_after = tracker.lock().clone();
2456        assert_eq!(
2457            events_after,
2458            vec![
2459                "setup:resource",
2460                "setup:outer",
2461                "cleanup:outer",
2462                "cleanup:resource"
2463            ],
2464            "Cleanups should run in LIFO order"
2465        );
2466    }
2467
2468    #[test]
2469    fn depends_cleanup_caching() {
2470        let ctx = test_context(None);
2471        let mut req = empty_request();
2472
2473        // First resolution
2474        let _dep1 = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
2475            &ctx, &mut req,
2476        ))
2477        .unwrap();
2478
2479        // Second resolution - should use cache
2480        let _dep2 = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
2481            &ctx, &mut req,
2482        ))
2483        .unwrap();
2484
2485        // Only one cleanup should be registered (due to caching)
2486        assert_eq!(ctx.cleanup_stack().len(), 1);
2487
2488        // Get tracker
2489        let tracker = ctx
2490            .dependency_cache()
2491            .get::<Arc<parking_lot::Mutex<Vec<String>>>>()
2492            .unwrap();
2493
2494        // Setup should only run once
2495        let events = tracker.lock().clone();
2496        assert_eq!(events, vec!["setup:resource"]);
2497    }
2498
2499    // ========================================================================
2500    // Scope Constraint Validation Tests (fastapi_rust-kpe)
2501    // ========================================================================
2502
2503    // Test: DependencyScopeError formatting
2504    #[test]
2505    fn scope_error_formatting() {
2506        let err = DependencyScopeError::new("CachedUser".to_string(), "DbConnection".to_string());
2507        let msg = err.to_string();
2508        assert!(msg.contains("Dependency scope violation"));
2509        assert!(msg.contains("request-scoped 'CachedUser'"));
2510        assert!(msg.contains("function-scoped 'DbConnection'"));
2511        assert!(msg.contains("cached value would become stale"));
2512    }
2513
2514    // Test: DependencyScopeError into_response
2515    #[test]
2516    fn scope_error_into_response() {
2517        let err = DependencyScopeError::new("A".to_string(), "B".to_string());
2518        let response = err.into_response();
2519        assert_eq!(response.status().as_u16(), 500);
2520    }
2521
2522    // Test: ResolutionStack detects request -> function scope violation
2523    #[test]
2524    fn resolution_stack_detects_scope_violation() {
2525        #[allow(dead_code)]
2526        struct RequestScoped;
2527        #[allow(dead_code)]
2528        struct FunctionScoped;
2529
2530        let stack = ResolutionStack::new();
2531
2532        // Push request-scoped dependency
2533        stack.push::<RequestScoped>("RequestScoped", DependencyScope::Request);
2534
2535        // Now try to resolve function-scoped - should detect violation
2536        let violation = stack.check_scope_violation("FunctionScoped", DependencyScope::Function);
2537        assert!(
2538            violation.is_some(),
2539            "Should detect request -> function scope violation"
2540        );
2541        let err = violation.unwrap();
2542        assert_eq!(err.request_scoped_type, "RequestScoped");
2543        assert_eq!(err.function_scoped_type, "FunctionScoped");
2544    }
2545
2546    // Test: ResolutionStack allows request -> request (valid)
2547    #[test]
2548    fn resolution_stack_allows_request_to_request() {
2549        #[allow(dead_code)]
2550        struct RequestA;
2551        #[allow(dead_code)]
2552        struct RequestB;
2553
2554        let stack = ResolutionStack::new();
2555
2556        // Push request-scoped dependency
2557        stack.push::<RequestA>("RequestA", DependencyScope::Request);
2558
2559        // Another request-scoped is OK
2560        let violation = stack.check_scope_violation("RequestB", DependencyScope::Request);
2561        assert!(violation.is_none(), "Request -> Request should be allowed");
2562    }
2563
2564    // Test: ResolutionStack allows function -> function (valid)
2565    #[test]
2566    fn resolution_stack_allows_function_to_function() {
2567        #[allow(dead_code)]
2568        struct FunctionA;
2569        #[allow(dead_code)]
2570        struct FunctionB;
2571
2572        let stack = ResolutionStack::new();
2573
2574        // Push function-scoped dependency
2575        stack.push::<FunctionA>("FunctionA", DependencyScope::Function);
2576
2577        // Another function-scoped is OK
2578        let violation = stack.check_scope_violation("FunctionB", DependencyScope::Function);
2579        assert!(
2580            violation.is_none(),
2581            "Function -> Function should be allowed"
2582        );
2583    }
2584
2585    // Test: ResolutionStack allows function -> request (valid)
2586    #[test]
2587    fn resolution_stack_allows_function_to_request() {
2588        #[allow(dead_code)]
2589        struct FunctionScoped;
2590        #[allow(dead_code)]
2591        struct RequestScoped;
2592
2593        let stack = ResolutionStack::new();
2594
2595        // Push function-scoped dependency
2596        stack.push::<FunctionScoped>("FunctionScoped", DependencyScope::Function);
2597
2598        // Request-scoped inner is OK (cached inner is fine for fresh outer)
2599        let violation = stack.check_scope_violation("RequestScoped", DependencyScope::Request);
2600        assert!(violation.is_none(), "Function -> Request should be allowed");
2601    }
2602
2603    // Test: Nested scope violation detection (A(request) -> B(request) -> C(function))
2604    #[test]
2605    fn resolution_stack_nested_scope_violation() {
2606        #[allow(dead_code)]
2607        struct OuterRequest;
2608        #[allow(dead_code)]
2609        struct MiddleRequest;
2610        #[allow(dead_code)]
2611        struct InnerFunction;
2612
2613        let stack = ResolutionStack::new();
2614
2615        // Push request -> request -> function
2616        stack.push::<OuterRequest>("OuterRequest", DependencyScope::Request);
2617        stack.push::<MiddleRequest>("MiddleRequest", DependencyScope::Request);
2618
2619        // Inner function-scoped should fail because there's a request-scoped in the chain
2620        let violation = stack.check_scope_violation("InnerFunction", DependencyScope::Function);
2621        assert!(violation.is_some(), "Should detect nested scope violation");
2622        let err = violation.unwrap();
2623        // Should report the closest request-scoped (MiddleRequest)
2624        assert_eq!(err.request_scoped_type, "MiddleRequest");
2625        assert_eq!(err.function_scoped_type, "InnerFunction");
2626    }
2627
2628    // Test: Empty stack has no scope violation
2629    #[test]
2630    fn resolution_stack_empty_no_scope_violation() {
2631        let stack = ResolutionStack::new();
2632
2633        // Empty stack should allow any scope
2634        let violation_fn = stack.check_scope_violation("SomeDep", DependencyScope::Function);
2635        let violation_req = stack.check_scope_violation("SomeDep", DependencyScope::Request);
2636
2637        assert!(
2638            violation_fn.is_none(),
2639            "Empty stack should allow function scope"
2640        );
2641        assert!(
2642            violation_req.is_none(),
2643            "Empty stack should allow request scope"
2644        );
2645    }
2646
2647    // Test: Scope violation in runtime dependency resolution (integration test)
2648    // This test verifies the actual panic behavior when scope rules are violated.
2649    //
2650    // NOTE: We can't easily test the panic in normal unit tests because catch_unwind
2651    // doesn't work well with async. Instead, we test the check_scope_violation method
2652    // directly in the tests above. The actual panic behavior is tested via the
2653    // Depends::from_request implementation.
2654
2655    // Test: Function-scoped dependency works correctly (NoCache config)
2656    #[test]
2657    fn function_scoped_resolves_fresh_each_time() {
2658        let ctx = test_context(None);
2659        let mut req = empty_request();
2660
2661        // Use NoCache (function scope)
2662        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
2663            &ctx, &mut req,
2664        ))
2665        .unwrap();
2666
2667        let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
2668            &ctx, &mut req,
2669        ))
2670        .unwrap();
2671
2672        let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
2673        assert_eq!(
2674            counter.load(Ordering::SeqCst),
2675            2,
2676            "Function-scoped should resolve 2 times (not cached)"
2677        );
2678    }
2679
2680    // Test: Request-scoped dependency is cached correctly
2681    #[test]
2682    fn request_scoped_cached_within_request() {
2683        let ctx = test_context(None);
2684        let mut req = empty_request();
2685
2686        // Use default config (request scope)
2687        let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
2688            .unwrap();
2689
2690        let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
2691            .unwrap();
2692
2693        let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
2694        assert_eq!(
2695            counter.load(Ordering::SeqCst),
2696            1,
2697            "Request-scoped should resolve only once (cached)"
2698        );
2699    }
2700
2701    // ========================================================================
2702    // Scope and Cleanup Integration Tests (bd-2290)
2703    // ========================================================================
2704
2705    /// Test that request-scoped dependencies with cleanup are only cleaned up once
2706    /// even when resolved multiple times
2707    #[test]
2708    fn request_scope_cleanup_only_once() {
2709        let ctx = test_context(None);
2710        let mut req = empty_request();
2711
2712        // Resolve the same cleanup dependency multiple times
2713        let _ = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
2714            &ctx, &mut req,
2715        ))
2716        .unwrap();
2717
2718        let _ = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
2719            &ctx, &mut req,
2720        ))
2721        .unwrap();
2722
2723        // Only one cleanup should be registered due to caching
2724        assert_eq!(
2725            ctx.cleanup_stack().len(),
2726            1,
2727            "Request-scoped should only register cleanup once"
2728        );
2729
2730        let tracker = ctx
2731            .dependency_cache()
2732            .get::<Arc<parking_lot::Mutex<Vec<String>>>>()
2733            .unwrap();
2734
2735        // Setup only called once
2736        assert_eq!(tracker.lock().len(), 1);
2737        assert_eq!(tracker.lock()[0], "setup:resource");
2738
2739        // Run cleanups
2740        futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
2741
2742        // Only one cleanup ran
2743        let events = tracker.lock().clone();
2744        assert_eq!(
2745            events,
2746            vec!["setup:resource", "cleanup:resource"],
2747            "Cleanup should run exactly once for cached dependency"
2748        );
2749    }
2750
2751    /// Test that function-scoped cleanup dependencies register cleanup for each resolution
2752    #[test]
2753    fn function_scope_cleanup_each_time() {
2754        // Create a function-scoped cleanup dependency
2755        #[derive(Clone)]
2756        #[allow(dead_code)]
2757        struct FunctionScopedWithCleanup {
2758            id: u32,
2759        }
2760
2761        impl FromDependencyWithCleanup for FunctionScopedWithCleanup {
2762            type Value = FunctionScopedWithCleanup;
2763            type Error = HttpError;
2764
2765            async fn setup(
2766                ctx: &RequestContext,
2767                _req: &mut Request,
2768            ) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
2769                // Get or create a cleanup counter
2770                let counter = ctx
2771                    .dependency_cache()
2772                    .get::<Arc<AtomicUsize>>()
2773                    .unwrap_or_else(|| {
2774                        let c = Arc::new(AtomicUsize::new(0));
2775                        ctx.dependency_cache().insert(Arc::clone(&c));
2776                        c
2777                    });
2778
2779                let cleanup_counter = Arc::clone(&counter);
2780                let cleanup = Box::new(move || {
2781                    Box::pin(async move {
2782                        cleanup_counter.fetch_add(1, Ordering::SeqCst);
2783                    }) as Pin<Box<dyn Future<Output = ()> + Send>>
2784                }) as CleanupFn;
2785
2786                Ok((FunctionScopedWithCleanup { id: 42 }, Some(cleanup)))
2787            }
2788        }
2789
2790        let ctx = test_context(None);
2791        let mut req = empty_request();
2792
2793        // Resolve 3 times with NoCache
2794        let _ = futures_executor::block_on(
2795            DependsCleanup::<FunctionScopedWithCleanup, NoCache>::from_request(&ctx, &mut req),
2796        )
2797        .unwrap();
2798
2799        let _ = futures_executor::block_on(
2800            DependsCleanup::<FunctionScopedWithCleanup, NoCache>::from_request(&ctx, &mut req),
2801        )
2802        .unwrap();
2803
2804        let _ = futures_executor::block_on(
2805            DependsCleanup::<FunctionScopedWithCleanup, NoCache>::from_request(&ctx, &mut req),
2806        )
2807        .unwrap();
2808
2809        // Should have 3 cleanups registered
2810        assert_eq!(
2811            ctx.cleanup_stack().len(),
2812            3,
2813            "Function-scoped should register cleanup each time"
2814        );
2815
2816        // Run all cleanups
2817        futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
2818
2819        // Verify all 3 cleanups ran
2820        let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
2821        assert_eq!(
2822            counter.load(Ordering::SeqCst),
2823            3,
2824            "All 3 cleanups should have run"
2825        );
2826    }
2827}