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;
8use std::sync::Arc;
9
10use solti_model::RunnerEnv;
11
12use crate::metrics::MetricsHandle;
13use crate::output::OutputRegistry;
14
15/// Shared build context passed to all runners.
16///
17/// Carries environment variables and a metrics handle that runners use during task construction.
18/// Created once at router setup time and shared (by clone) across all [`Runner::build_task`](crate::Runner::build_task) calls.
19///
20/// ## Defaults
21///
22/// - `env`: empty [`RunnerEnv`]
23/// - `metrics`: [`NoOpMetrics`](crate::NoOpMetrics) (zero-cost)
24///
25/// ## Also
26///
27/// - [`RunnerRouter::with_context`](crate::RunnerRouter::with_context) sets the context for all runners.
28/// - [`MetricsHandle`](crate::MetricsHandle) - `Arc<dyn MetricsBackend>`.
29#[derive(Clone)]
30pub struct BuildContext {
31    output_registry: Arc<OutputRegistry>,
32    metrics: MetricsHandle,
33    env: RunnerEnv,
34}
35
36impl BuildContext {
37    /// Create a new build context with the given params.
38    pub fn new(env: RunnerEnv, metrics: MetricsHandle) -> Self {
39        Self {
40            env,
41            metrics,
42            output_registry: Arc::new(OutputRegistry::default()),
43        }
44    }
45
46    /// Get a reference to the shared environment.
47    pub fn env(&self) -> &RunnerEnv {
48        &self.env
49    }
50
51    /// Get a clonable handle to the metrics backend.
52    pub fn metrics(&self) -> &MetricsHandle {
53        &self.metrics
54    }
55
56    /// Get a shared handle to the output registry.
57    pub fn output_registry(&self) -> &Arc<OutputRegistry> {
58        &self.output_registry
59    }
60
61    /// Replace the environment and return updated context.
62    pub fn with_env(mut self, env: RunnerEnv) -> Self {
63        self.env = env;
64        self
65    }
66
67    /// Replace the metrics backend and return updated context.
68    pub fn with_metrics(mut self, metrics: MetricsHandle) -> Self {
69        self.metrics = metrics;
70        self
71    }
72
73    /// Replace the output registry and return updated context.
74    pub fn with_output_registry(mut self, registry: Arc<OutputRegistry>) -> Self {
75        self.output_registry = registry;
76        self
77    }
78}
79
80impl Default for BuildContext {
81    fn default() -> Self {
82        Self {
83            env: RunnerEnv::default(),
84            metrics: crate::metrics::noop_metrics(),
85            output_registry: Arc::new(OutputRegistry::default()),
86        }
87    }
88}
89
90impl fmt::Debug for BuildContext {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.debug_struct("BuildContext")
93            .field("env_len", &self.env.len())
94            .field("metrics", &"<handle>")
95            .finish()
96    }
97}
98
99impl fmt::Display for BuildContext {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "BuildContext(env_len={})", self.env.len())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use std::sync::Arc;
108
109    use super::BuildContext;
110    use crate::OutputRegistry;
111
112    use solti_model::{RunnerEnv, TaskId};
113
114    #[test]
115    fn default_build_context_has_empty_env_and_noop_metrics() {
116        let ctx = BuildContext::default();
117        assert_eq!(ctx.env().len(), 0);
118    }
119
120    #[test]
121    fn new_uses_provided_env_and_metrics() {
122        let mut env = RunnerEnv::new();
123        env.push("FOO", "bar");
124        env.push("BAZ", "qux");
125
126        let metrics = crate::metrics::noop_metrics();
127        let ctx = BuildContext::new(env.clone(), metrics);
128
129        assert_eq!(ctx.env().len(), env.len());
130        assert_eq!(ctx.env().get("FOO"), Some("bar"));
131        assert_eq!(ctx.env().get("BAZ"), Some("qux"));
132    }
133
134    #[test]
135    fn with_env_replaces_existing_env() {
136        let mut env1 = RunnerEnv::new();
137        env1.push("FOO", "one");
138
139        let mut env2 = RunnerEnv::new();
140        env2.push("BAR", "two");
141
142        let metrics = crate::metrics::noop_metrics();
143        let ctx = BuildContext::new(env1, metrics).with_env(env2.clone());
144
145        assert_eq!(ctx.env().len(), env2.len());
146        assert!(ctx.env().get("FOO").is_none());
147        assert_eq!(ctx.env().get("BAR"), Some("two"));
148    }
149
150    #[test]
151    fn with_metrics_replaces_backend() {
152        let env = RunnerEnv::new();
153        let metrics1 = crate::metrics::noop_metrics();
154        let metrics2 = crate::metrics::noop_metrics();
155
156        let ctx = BuildContext::new(env, metrics1).with_metrics(metrics2);
157
158        ctx.metrics()
159            .record_task_started(crate::RunnerType::Subprocess);
160    }
161
162    #[test]
163    fn display_includes_env_length() {
164        let mut env = RunnerEnv::new();
165        env.push("FOO", "bar");
166
167        let metrics = crate::metrics::noop_metrics();
168        let ctx = BuildContext::new(env, metrics);
169
170        let s = ctx.to_string();
171        assert_eq!(s, "BuildContext(env_len=1)");
172    }
173
174    #[test]
175    fn metrics_handle_can_be_cloned() {
176        let ctx = BuildContext::default();
177        let handle = ctx.metrics().clone();
178
179        handle.record_task_started(crate::RunnerType::Subprocess);
180        handle.record_task_completed(
181            crate::RunnerType::Subprocess,
182            crate::TaskOutcome::Success,
183            100,
184        );
185    }
186    #[test]
187    fn default_build_context_has_an_empty_output_registry() {
188        let ctx = BuildContext::default();
189        assert_eq!(ctx.output_registry().active_channels(), 0);
190    }
191
192    #[test]
193    fn with_output_registry_replaces_registry() {
194        let custom = Arc::new(OutputRegistry::new(2048));
195        let _ = custom.sink_for(TaskId::from("seed"), 1);
196
197        let ctx = BuildContext::default().with_output_registry(custom.clone());
198
199        assert_eq!(ctx.output_registry().active_channels(), 1);
200        assert!(Arc::ptr_eq(ctx.output_registry(), &custom));
201    }
202
203    #[test]
204    fn output_registry_handle_is_shared_via_arc() {
205        let ctx = BuildContext::default();
206        let task = TaskId::from("shared");
207        let _sink = ctx.output_registry().sink_for(task.clone(), 1);
208
209        let handle = Arc::clone(ctx.output_registry());
210        assert!(handle.subscribe(&task).is_some());
211    }
212}