Skip to main content

solti_runner/
context.rs

1//! # Build context.
2//!
3//! [`BuildContext`] carries shared dependencies (environment variables, metrics handle) injected into runners at task-build time.
4//!
5//! See [`Runner::build_task`](crate::Runner::build_task) for usage.
6
7use std::fmt;
8
9use solti_model::RunnerEnv;
10
11use crate::metrics::MetricsHandle;
12
13/// Shared build context passed to all runners.
14///
15/// Carries environment variables and a metrics handle that runners use during task construction.
16/// Created once at router setup time and shared (by clone) across all [`Runner::build_task`](crate::Runner::build_task) calls.
17///
18/// ## Defaults
19///
20/// - `env`: empty [`RunnerEnv`]
21/// - `metrics`: [`NoOpMetrics`](crate::NoOpMetrics) (zero-cost)
22///
23/// ## Also
24///
25/// - [`RunnerRouter::with_context`](crate::RunnerRouter::with_context) sets the context for all runners.
26/// - [`MetricsHandle`](crate::MetricsHandle) - `Arc<dyn MetricsBackend>`.
27#[derive(Clone)]
28pub struct BuildContext {
29    metrics: MetricsHandle,
30    env: RunnerEnv,
31}
32
33impl BuildContext {
34    /// Create a new build context with the given params.
35    pub fn new(env: RunnerEnv, metrics: MetricsHandle) -> Self {
36        Self { env, metrics }
37    }
38
39    /// Get a reference to the shared environment.
40    pub fn env(&self) -> &RunnerEnv {
41        &self.env
42    }
43
44    /// Get a clonable handle to the metrics backend.
45    pub fn metrics(&self) -> &MetricsHandle {
46        &self.metrics
47    }
48
49    /// Replace the environment and return updated context.
50    pub fn with_env(mut self, env: RunnerEnv) -> Self {
51        self.env = env;
52        self
53    }
54
55    /// Replace the metrics backend and return updated context.
56    pub fn with_metrics(mut self, metrics: MetricsHandle) -> Self {
57        self.metrics = metrics;
58        self
59    }
60}
61
62impl Default for BuildContext {
63    fn default() -> Self {
64        Self {
65            env: RunnerEnv::default(),
66            metrics: crate::metrics::noop_metrics(),
67        }
68    }
69}
70
71impl fmt::Debug for BuildContext {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("BuildContext")
74            .field("env_len", &self.env.len())
75            .field("metrics", &"<handle>")
76            .finish()
77    }
78}
79
80impl fmt::Display for BuildContext {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "BuildContext(env_len={})", self.env.len())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::BuildContext;
89    use solti_model::RunnerEnv;
90
91    #[test]
92    fn default_build_context_has_empty_env_and_noop_metrics() {
93        let ctx = BuildContext::default();
94        assert_eq!(ctx.env().len(), 0);
95    }
96
97    #[test]
98    fn new_uses_provided_env_and_metrics() {
99        let mut env = RunnerEnv::new();
100        env.push("FOO", "bar");
101        env.push("BAZ", "qux");
102
103        let metrics = crate::metrics::noop_metrics();
104        let ctx = BuildContext::new(env.clone(), metrics);
105
106        assert_eq!(ctx.env().len(), env.len());
107        assert_eq!(ctx.env().get("FOO"), Some("bar"));
108        assert_eq!(ctx.env().get("BAZ"), Some("qux"));
109    }
110
111    #[test]
112    fn with_env_replaces_existing_env() {
113        let mut env1 = RunnerEnv::new();
114        env1.push("FOO", "one");
115
116        let mut env2 = RunnerEnv::new();
117        env2.push("BAR", "two");
118
119        let metrics = crate::metrics::noop_metrics();
120        let ctx = BuildContext::new(env1, metrics).with_env(env2.clone());
121
122        assert_eq!(ctx.env().len(), env2.len());
123        assert!(ctx.env().get("FOO").is_none());
124        assert_eq!(ctx.env().get("BAR"), Some("two"));
125    }
126
127    #[test]
128    fn with_metrics_replaces_backend() {
129        let env = RunnerEnv::new();
130        let metrics1 = crate::metrics::noop_metrics();
131        let metrics2 = crate::metrics::noop_metrics();
132
133        let ctx = BuildContext::new(env, metrics1).with_metrics(metrics2);
134
135        ctx.metrics()
136            .record_task_started(crate::RunnerType::Subprocess);
137    }
138
139    #[test]
140    fn display_includes_env_length() {
141        let mut env = RunnerEnv::new();
142        env.push("FOO", "bar");
143
144        let metrics = crate::metrics::noop_metrics();
145        let ctx = BuildContext::new(env, metrics);
146
147        let s = ctx.to_string();
148        assert_eq!(s, "BuildContext(env_len=1)");
149    }
150
151    #[test]
152    fn metrics_handle_can_be_cloned() {
153        let ctx = BuildContext::default();
154        let handle = ctx.metrics().clone();
155
156        handle.record_task_started(crate::RunnerType::Subprocess);
157        handle.record_task_completed(
158            crate::RunnerType::Subprocess,
159            crate::TaskOutcome::Success,
160            100,
161        );
162    }
163}