1use std::fmt;
8use std::sync::Arc;
9
10use solti_model::RunnerEnv;
11
12use crate::metrics::MetricsHandle;
13use crate::output::OutputRegistry;
14
15#[derive(Clone)]
30pub struct BuildContext {
31 output_registry: Arc<OutputRegistry>,
32 metrics: MetricsHandle,
33 env: RunnerEnv,
34}
35
36impl BuildContext {
37 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 pub fn env(&self) -> &RunnerEnv {
48 &self.env
49 }
50
51 pub fn metrics(&self) -> &MetricsHandle {
53 &self.metrics
54 }
55
56 pub fn output_registry(&self) -> &Arc<OutputRegistry> {
58 &self.output_registry
59 }
60
61 pub fn with_env(mut self, env: RunnerEnv) -> Self {
63 self.env = env;
64 self
65 }
66
67 pub fn with_metrics(mut self, metrics: MetricsHandle) -> Self {
69 self.metrics = metrics;
70 self
71 }
72
73 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}