1use crate::config::{Config, ConfigProvider, ConfigSource};
7
8use crate::phase_runner::{DefaultPhaseRunner, PhaseRunner};
9use crate::result_processor::{DefaultResultProcessor, ResultProcessor};
10
11use crate::error::Error;
12use crate::http;
13use crate::profiles::UserProfile;
15use crate::storage::{LoadHistory, SaveResult};
16use crate::task_runner::TestRunResult;
17use crate::terminal;
18use crate::types::TestResult;
19
20#[derive(Clone)]
22pub(crate) struct EarlyExitFlags {
23 pub(crate) show_config_path: bool,
24 pub(crate) generate_completion: Option<crate::cli::ShellType>,
25 pub(crate) history: bool,
26 pub(crate) dry_run: bool,
27}
28
29impl EarlyExitFlags {
30 pub(crate) fn from_args(args: &crate::cli::Args) -> Self {
31 Self {
32 show_config_path: args.show_config_path,
33 generate_completion: args.generate_completion,
34 history: args.history,
35 dry_run: args.dry_run,
36 }
37 }
38}
39
40pub struct StorageBuilder {
42 saver: Option<std::sync::Arc<dyn SaveResult + Send + Sync>>,
43 history: Option<std::sync::Arc<dyn LoadHistory + Send + Sync>>,
44}
45
46impl StorageBuilder {
47 pub fn new() -> Self {
48 Self {
49 saver: None,
50 history: None,
51 }
52 }
53
54 pub fn with_saver(mut self, saver: impl SaveResult + 'static) -> Self {
55 self.saver = Some(std::sync::Arc::new(saver));
56 self
57 }
58
59 pub fn with_saver_arc(mut self, saver: std::sync::Arc<dyn SaveResult + Send + Sync>) -> Self {
60 self.saver = Some(saver);
61 self
62 }
63
64 pub fn with_history(mut self, history: impl LoadHistory + 'static) -> Self {
65 self.history = Some(std::sync::Arc::new(history));
66 self
67 }
68
69 pub fn with_history_arc(
70 mut self,
71 history: std::sync::Arc<dyn LoadHistory + Send + Sync>,
72 ) -> Self {
73 self.history = Some(history);
74 self
75 }
76
77 fn build(
79 self,
80 ) -> (
81 std::sync::Arc<dyn SaveResult + Send + Sync>,
82 std::sync::Arc<dyn LoadHistory + Send + Sync>,
83 ) {
84 let saver = self.saver.unwrap_or_else(|| {
85 std::sync::Arc::new(crate::storage::FileStorage::new())
86 as std::sync::Arc<dyn SaveResult + Send + Sync>
87 });
88 let history = self.history.unwrap_or_else(|| {
89 std::sync::Arc::new(crate::storage::FileStorage::new())
90 as std::sync::Arc<dyn LoadHistory + Send + Sync>
91 });
92 (saver, history)
93 }
94}
95
96impl Default for StorageBuilder {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102pub struct Orchestrator {
107 pub(crate) config: std::sync::Arc<dyn ConfigProvider>,
108 pub(crate) client: reqwest::Client,
109
110 early_exit: EarlyExitFlags,
111 saver: std::sync::Arc<dyn SaveResult + Send + Sync>,
112 history: std::sync::Arc<dyn LoadHistory + Send + Sync>,
113 processor: std::sync::Arc<dyn ResultProcessor + Send + Sync>,
114
115 phase_runner: std::sync::Arc<dyn PhaseRunner + Send + Sync>,
116 services: std::sync::Arc<dyn crate::services::Services>,
117}
118
119impl Orchestrator {
120 fn cfg(&self) -> &Config {
122 self.config.config()
123 }
124
125 pub fn new(
127 args: crate::cli::Args,
128 file_config: Option<crate::config::File>,
129 ) -> Result<Self, Error> {
130 let source = ConfigSource::from_args(&args);
131
132 let (config, profile_validation) =
133 Config::from_args_with_file(&source, file_config.clone());
134
135 for warning in &profile_validation.warnings {
136 eprintln!("Warning: {warning}");
137 }
138
139 let file_validation = config.validate_and_report(&source, file_config);
140 for error in &file_validation.errors {
141 eprintln!("Error: {error}");
142 }
143
144 let combined_valid = profile_validation.valid && file_validation.valid;
145 if config.strict() && !combined_valid {
146 return Err(Error::Context {
147 msg: "Configuration validation failed".to_string(),
148 source: None,
149 });
150 }
151
152 let early_exit = EarlyExitFlags::from_args(&args);
153 Self::from_config(config, early_exit)
154 }
155
156 pub(crate) fn from_config(config: Config, early_exit: EarlyExitFlags) -> Result<Self, Error> {
158 Self::from_config_with_storage(config, early_exit, StorageBuilder::new())
159 }
160
161 pub(crate) fn from_config_with_storage(
163 config: Config,
164 early_exit: EarlyExitFlags,
165 storage: StorageBuilder,
166 ) -> Result<Self, Error> {
167 let http_settings = http::Settings::from(&config);
168 let client = http::create_client(&http_settings)?;
169
170 let (saver, history) = storage.build();
171 let services = std::sync::Arc::new(crate::services::ServiceContainer::new(client.clone()));
172
173 Ok(Self {
174 config: std::sync::Arc::new(config),
175 client,
176
177 early_exit,
178 saver,
179 history,
180 processor: std::sync::Arc::new(DefaultResultProcessor),
181 phase_runner: std::sync::Arc::new(DefaultPhaseRunner::new()),
182 services,
183 })
184 }
185
186 #[must_use]
188 pub fn services(&self) -> &dyn crate::services::Services {
189 self.services.as_ref()
190 }
191
192 pub fn services_arc(&self) -> std::sync::Arc<dyn crate::services::Services> {
194 self.services.clone()
195 }
196
197 pub async fn run(&self) -> Result<(), Error> {
199 self.phase_runner.run_all(self).await
200 }
201
202 #[must_use]
204 pub fn is_verbose(&self) -> bool {
205 if self.cfg().quiet() {
206 return false;
207 }
208 let format_non_verbose = self.cfg().format().is_some_and(|f| f.is_non_verbose());
209 !self.cfg().simple()
210 && !self.cfg().json()
211 && !self.cfg().csv()
212 && !self.cfg().list()
213 && !format_non_verbose
214 }
215
216 #[must_use]
218 pub fn is_simple_mode(&self) -> bool {
219 self.cfg().simple()
220 || self.cfg().quiet()
221 || self.cfg().format() == Some(crate::config::Format::Simple)
222 }
223
224 #[must_use]
226 pub fn config(&self) -> &Config {
227 self.cfg()
228 }
229
230 #[must_use]
232 pub(crate) fn early_exit(&self) -> &EarlyExitFlags {
233 &self.early_exit
234 }
235
236 #[must_use]
238 pub fn saver(&self) -> &dyn SaveResult {
239 self.saver.as_ref()
240 }
241
242 #[must_use]
244 pub fn history(&self) -> &dyn LoadHistory {
245 self.history.as_ref()
246 }
247
248 pub fn http_client(&self) -> &reqwest::Client {
250 &self.client
251 }
252
253 pub(crate) fn output_results(
255 &self,
256 result: &mut TestResult,
257 dl_result: &TestRunResult,
258 ul_result: &TestRunResult,
259 elapsed: std::time::Duration,
260 ) -> Result<(), Error> {
261 let profile = UserProfile::from_name(self.cfg().profile().unwrap_or("power-user"))
262 .unwrap_or(UserProfile::PowerUser);
263 self.processor.process(result, profile);
265
266 let output_format = crate::output_strategy::resolve_output_format(
267 self.cfg(),
268 dl_result,
269 ul_result,
270 elapsed,
271 );
272
273 if self.is_verbose() {
274 self.reveal_results(result, self.cfg().theme(), profile);
275 }
276
277 output_format.format(result, self.cfg().bytes())?;
278 Ok(())
279 }
280
281 fn reveal_results(
283 &self,
284 result: &TestResult,
285 theme: crate::theme::Theme,
286 profile: UserProfile,
287 ) {
288 let nc = terminal::no_color();
289
290 let sample_count = result.download_samples.as_ref().map_or(0, Vec::len)
291 + result.upload_samples.as_ref().map_or(0, Vec::len)
292 + result.ping_samples.as_ref().map_or(0, Vec::len);
293
294 let overall_grade = crate::grades::grade_overall(
295 result.ping,
296 result.jitter,
297 result.download,
298 result.upload,
299 profile,
300 );
301
302 let grade_badge = overall_grade.color_str(nc, theme);
303 let grade_plain = overall_grade.as_str().to_string();
304 crate::progress::reveal_scan_complete(sample_count, &grade_badge, &grade_plain, nc, theme);
305 crate::progress::reveal_pause();
306 }
307
308 fn print_kv(nc: bool, key: &str, value: &str) {
309 if nc {
310 eprintln!(" {key}: {value}");
311 } else {
312 use owo_colors::OwoColorize;
313 eprintln!(" {}: {}", key.dimmed(), value.cyan());
314 }
315 }
316
317 pub(crate) fn run_dry_run(&self) {
319 let nc = terminal::no_color();
320 let config = self.config();
321
322 if nc {
323 eprintln!("Configuration valid:");
324 } else {
325 use owo_colors::OwoColorize;
326 eprintln!("{}", "Configuration valid:".green().bold());
327 }
328
329 Self::print_kv(nc, "Timeout", &format!("{}s", config.timeout()));
330 Self::print_kv(nc, "Format", self.format_description());
331 if config.quiet() {
332 Self::print_kv(nc, "Quiet", "enabled");
333 }
334 if let Some(source) = config.source() {
335 Self::print_kv(nc, "Source IP", source);
336 }
337 if config.no_download() {
338 Self::print_kv(nc, "Download test", "disabled");
339 }
340 if config.no_upload() {
341 Self::print_kv(nc, "Upload test", "disabled");
342 }
343 if config.single() {
344 Self::print_kv(nc, "Streams", "single");
345 }
346 if let Some(ca_cert) = config.ca_cert() {
347 Self::print_kv(nc, "CA cert", ca_cert);
348 }
349 if let Some(tls_version) = config.tls_version() {
350 Self::print_kv(nc, "TLS version", tls_version);
351 }
352 if config.pin_certs() {
353 Self::print_kv(nc, "TLS domain restriction", "speedtest.net/ookla.com");
354 }
355
356 if nc {
357 eprintln!("\nDry run complete. Run without --dry-run to perform speed test.");
358 } else {
359 use owo_colors::OwoColorize;
360 eprintln!(
361 "\n{}",
362 "Dry run complete. Run without --dry-run to perform speed test.".bright_black()
363 );
364 }
365 }
366
367 fn format_description(&self) -> &'static str {
369 match self.cfg().format() {
370 Some(f) => f.label(),
371 None => "Detailed (default)",
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::config::{ConfigSource, OutputSource};
380
381 fn orch_from_source(source: &ConfigSource, early_exit: EarlyExitFlags) -> Orchestrator {
382 let config = Config::from_source(source);
383 Orchestrator::from_config(config, early_exit).unwrap()
384 }
385
386 fn default_early_exit() -> EarlyExitFlags {
387 EarlyExitFlags {
388 show_config_path: false,
389 generate_completion: None,
390 history: false,
391 dry_run: false,
392 }
393 }
394
395 #[test]
396 fn test_orchestrator_creation() {
397 let source = ConfigSource::default();
398 let config = Config::from_source(&source);
399 let orch = Orchestrator::from_config(config, default_early_exit());
400 assert!(orch.is_ok());
401 }
402
403 #[test]
404 fn test_is_verbose_default() {
405 let source = ConfigSource::default();
406 let orch = orch_from_source(&source, default_early_exit());
407 assert!(orch.is_verbose());
408 }
409
410 #[test]
411 fn test_is_verbose_quiet() {
412 let source = ConfigSource {
413 output: OutputSource {
414 quiet: Some(true),
415 ..Default::default()
416 },
417 ..Default::default()
418 };
419 let orch = orch_from_source(&source, default_early_exit());
420 assert!(!orch.is_verbose());
421 }
422
423 #[test]
424 fn test_is_simple_mode_default() {
425 let source = ConfigSource::default();
426 let orch = orch_from_source(&source, default_early_exit());
427 assert!(!orch.is_simple_mode());
428 }
429
430 #[test]
431 fn test_is_simple_mode_simple() {
432 let source = ConfigSource {
433 output: OutputSource {
434 simple: Some(true),
435 ..Default::default()
436 },
437 ..Default::default()
438 };
439 let orch = orch_from_source(&source, default_early_exit());
440 assert!(orch.is_simple_mode());
441 }
442
443 #[test]
444 fn test_dry_run_succeeds() {
445 let source = ConfigSource::default();
446 let early_exit = EarlyExitFlags {
447 dry_run: true,
448 ..default_early_exit()
449 };
450 let orch = orch_from_source(&source, early_exit);
451 orch.run_dry_run();
452 }
453
454 #[test]
455 fn test_early_exit_flags_default() {
456 let flags = default_early_exit();
457 assert!(!flags.show_config_path);
458 assert!(flags.generate_completion.is_none());
459 assert!(!flags.history);
460 assert!(!flags.dry_run);
461 }
462
463 #[test]
464 fn test_storage_builder_defaults() {
465 let shared = std::sync::Arc::new(crate::storage::MockStorage::new());
466 let saver = shared.clone() as std::sync::Arc<dyn crate::storage::SaveResult + Send + Sync>;
467 let history =
468 shared.clone() as std::sync::Arc<dyn crate::storage::LoadHistory + Send + Sync>;
469
470 let builder = StorageBuilder::new()
471 .with_saver_arc(saver)
472 .with_history_arc(history);
473 let (saver, history) = builder.build();
474 <dyn crate::storage::SaveResult>::save(&*saver, &crate::types::TestResult::default())
475 .unwrap();
476 let _ = <dyn crate::storage::LoadHistory>::load_recent(&*history, 1);
477 }
478
479 #[test]
480 fn test_storage_builder_custom() {
481 let shared = std::sync::Arc::new(crate::storage::MockStorage::new());
482 let saver = shared.clone() as std::sync::Arc<dyn crate::storage::SaveResult + Send + Sync>;
483 let history =
484 shared.clone() as std::sync::Arc<dyn crate::storage::LoadHistory + Send + Sync>;
485
486 let builder = StorageBuilder::new()
487 .with_saver_arc(saver)
488 .with_history_arc(history);
489
490 let (saver, history) = builder.build();
491
492 let result = crate::types::TestResult::default();
493 <dyn crate::storage::SaveResult>::save(&*saver, &result).unwrap();
494 let loaded = <dyn crate::storage::LoadHistory>::load_recent(&*history, 10).unwrap();
495 assert_eq!(loaded.len(), 1);
496 }
497
498 #[test]
499 fn test_orchestrator_exposes_services() {
500 let args = crate::cli::Args::default();
501 let orch = Orchestrator::new(args, None).unwrap();
502 let _services = orch.services();
503 }
504
505 fn dry_run_orch(
508 output: OutputSource,
509 test: crate::config::TestSource,
510 network: crate::config::NetworkSource,
511 ) -> Orchestrator {
512 let source = ConfigSource {
513 output,
514 test,
515 network,
516 ..Default::default()
517 };
518 orch_from_source(
519 &source,
520 EarlyExitFlags {
521 dry_run: true,
522 ..default_early_exit()
523 },
524 )
525 }
526
527 #[test]
528 fn test_dry_run_no_color_mode() {
529 let orch = dry_run_orch(Default::default(), Default::default(), Default::default());
531 orch.run_dry_run(); }
533
534 #[test]
535 fn test_dry_run_quiet_branch() {
536 let orch = dry_run_orch(
537 OutputSource {
538 quiet: Some(true),
539 ..Default::default()
540 },
541 Default::default(),
542 Default::default(),
543 );
544 orch.run_dry_run();
545 }
546
547 #[test]
548 fn test_dry_run_no_download_branch() {
549 let orch = dry_run_orch(
550 Default::default(),
551 crate::config::TestSource {
552 no_download: Some(true),
553 ..Default::default()
554 },
555 Default::default(),
556 );
557 orch.run_dry_run();
558 }
559
560 #[test]
561 fn test_dry_run_no_upload_branch() {
562 let orch = dry_run_orch(
563 Default::default(),
564 crate::config::TestSource {
565 no_upload: Some(true),
566 ..Default::default()
567 },
568 Default::default(),
569 );
570 orch.run_dry_run();
571 }
572
573 #[test]
574 fn test_dry_run_single_stream_branch() {
575 let orch = dry_run_orch(
576 Default::default(),
577 crate::config::TestSource {
578 single: Some(true),
579 ..Default::default()
580 },
581 Default::default(),
582 );
583 orch.run_dry_run();
584 }
585
586 #[test]
587 #[ignore = "requires a bound local IP; tested in http::tests"]
588 fn test_dry_run_source_ip_branch() {
589 let orch = dry_run_orch(
590 Default::default(),
591 Default::default(),
592 crate::config::NetworkSource {
593 source: Some("127.0.0.1:0".to_string()),
594 ..Default::default()
595 },
596 );
597 orch.run_dry_run();
598 }
599
600 #[test]
601 #[ignore = "requires Rustls CryptoProvider; tested in http::tests"]
602 fn test_dry_run_tls_version_branch() {
603 let orch = dry_run_orch(
604 Default::default(),
605 Default::default(),
606 crate::config::NetworkSource {
607 tls_version: Some("1.3".to_string()),
608 ..Default::default()
609 },
610 );
611 orch.run_dry_run();
612 }
613
614 #[test]
615 #[ignore = "requires Rustls CryptoProvider; tested in http::tests"]
616 fn test_dry_run_pin_certs_branch() {
617 let orch = dry_run_orch(
618 Default::default(),
619 Default::default(),
620 crate::config::NetworkSource {
621 pin_certs: Some(true),
622 ..Default::default()
623 },
624 );
625 orch.run_dry_run();
626 }
627}