Skip to main content

xianyun_executor/exec/
context.rs

1//! Attaching a [`super::CommandRunner`] to an ambient execution context.
2//!
3//! Ported from `internal/executor/context.go`.
4//!
5//! Go's `context.Context` is an explicit value threaded through every
6//! call, with `context.WithValue`/`ctx.Value` as a typed, per-request key/
7//! value store. Rust has no first-class equivalent: the idiomatic
8//! translation is usually to pass the `CommandRunner` explicitly (as a
9//! function parameter or struct field) rather than smuggle it through an
10//! ambient context object. This module is ported as a narrow
11//! request-scoped container (`Context`) to preserve the ported tests'
12//! shape during the red phase; Phase 4 should revisit whether callers can
13//! be refactored to pass the runner explicitly instead. See
14//! `PORT_NOTES.md`.
15
16use std::fmt;
17use std::sync::Arc;
18
19use super::CommandRunner;
20
21/// Ported from `ErrRunnerMissing`.
22#[derive(Debug)]
23pub struct ErrRunnerMissing;
24
25impl fmt::Display for ErrRunnerMissing {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "command runner missing from context")
28    }
29}
30
31impl std::error::Error for ErrRunnerMissing {}
32
33/// A narrow stand-in for Go's `context.Context` carrying a single
34/// [`CommandRunner`] value. Unlike Go's context tree, this does not
35/// support cancellation, deadlines, or arbitrary key/value nesting --
36/// those concerns are out of scope for this port (see `PORT_NOTES.md`).
37#[derive(Clone, Default)]
38pub struct Context {
39    runner: Option<Arc<dyn CommandRunner + Send + Sync>>,
40}
41
42/// Ported from `WithContext`.
43pub fn with_context(_ctx: Context, runner: Arc<dyn CommandRunner + Send + Sync>) -> Context {
44    Context {
45        runner: Some(runner),
46    }
47}
48
49/// Ported from `FromContext`. Panics with [`ErrRunnerMissing`] if no
50/// runner is on the context, matching the Go behavior.
51pub fn from_context(ctx: &Context) -> Arc<dyn CommandRunner + Send + Sync> {
52    match &ctx.runner {
53        Some(runner) => runner.clone(),
54        None => panic!("{ErrRunnerMissing}"),
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::exec::mock::MockCommandRunner;
62
63    #[test]
64    fn panics_on_empty_context() {
65        let ctx = Context::default();
66
67        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| from_context(&ctx)));
68        assert!(result.is_err());
69    }
70
71    #[test]
72    fn same_from_used_context() {
73        let runner: Arc<dyn CommandRunner + Send + Sync> = Arc::new(MockCommandRunner::new());
74        let ctx = with_context(Context::default(), runner.clone());
75
76        let runner1 = from_context(&ctx);
77        let runner2 = from_context(&ctx);
78
79        assert!(
80            Arc::ptr_eq(&runner, &runner1),
81            "FromContext should return original client"
82        );
83        assert!(
84            Arc::ptr_eq(&runner, &runner2),
85            "FromContext should return original client"
86        );
87    }
88
89    #[test]
90    fn nested_context() {
91        // Go nests via context.WithCancel; this port's Context has no
92        // cancellation tree to nest, so nesting is modeled as a clone
93        // (Context is designed to be cheaply Clone-able, matching how a
94        // child Go context still resolves values from its parent).
95        let runner: Arc<dyn CommandRunner + Send + Sync> = Arc::new(MockCommandRunner::new());
96        let ctx = with_context(Context::default(), runner.clone());
97        let nested = ctx.clone();
98
99        let runner1 = from_context(&nested);
100        assert!(
101            Arc::ptr_eq(&runner, &runner1),
102            "FromContext should return original client from nested context"
103        );
104    }
105}