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 pub(crate) fn run_dry_run(&self) {
310 let nc = terminal::no_color();
311 let config = self.config();
312
313 if nc {
314 eprintln!("Configuration valid:");
315 eprintln!(" Timeout: {}s", config.timeout());
316 eprintln!(" Format: {}", self.format_description());
317 if config.quiet() {
318 eprintln!(" Quiet: enabled");
319 }
320 if let Some(source) = config.source() {
321 eprintln!(" Source IP: {source}");
322 }
323 if config.no_download() {
324 eprintln!(" Download test: disabled");
325 }
326 if config.no_upload() {
327 eprintln!(" Upload test: disabled");
328 }
329 if config.single() {
330 eprintln!(" Streams: single");
331 }
332 if let Some(ca_cert) = config.ca_cert() {
333 eprintln!(" CA cert: {ca_cert}");
334 }
335 if let Some(tls_version) = config.tls_version() {
336 eprintln!(" TLS version: {tls_version}");
337 }
338 if config.pin_certs() {
339 eprintln!(" Cert pinning: enabled");
340 }
341 eprintln!("\nDry run complete. Run without --dry-run to perform speed test.");
342 } else {
343 use owo_colors::OwoColorize;
344
345 eprintln!("{}", "Configuration valid:".green().bold());
346 eprintln!(
347 " {}: {}s",
348 "Timeout".dimmed(),
349 config.timeout().to_string().cyan()
350 );
351 eprintln!(
352 " {}: {}",
353 "Format".dimmed(),
354 self.format_description().white()
355 );
356 if config.quiet() {
357 eprintln!(" {}: {}", "Quiet".dimmed(), "enabled".green());
358 }
359 if let Some(source) = config.source() {
360 eprintln!(" {}: {source}", "Source IP".dimmed());
361 }
362 if config.no_download() {
363 eprintln!(" {}: {}", "Download test".dimmed(), "disabled".yellow());
364 }
365 if config.no_upload() {
366 eprintln!(" {}: {}", "Upload test".dimmed(), "disabled".yellow());
367 }
368 if config.single() {
369 eprintln!(" {}: {}", "Streams".dimmed(), "single".yellow());
370 }
371 if let Some(ca_cert) = config.ca_cert() {
372 eprintln!(" {}: {ca_cert}", "CA cert".dimmed());
373 }
374 if let Some(tls_version) = config.tls_version() {
375 eprintln!(" {}: {tls_version}", "TLS version".dimmed());
376 }
377 if config.pin_certs() {
378 eprintln!(" {}: {}", "Cert pinning".dimmed(), "enabled".yellow());
379 }
380 eprintln!(
381 "\n{}",
382 "Dry run complete. Run without --dry-run to perform speed test.".bright_black()
383 );
384 }
385 }
386
387 fn format_description(&self) -> &'static str {
389 match self.cfg().format() {
390 Some(f) => f.label(),
391 None => "Detailed (default)",
392 }
393 }
394}
395
396pub trait ConfigAccessor: Send {
402 fn config(&self) -> &Config;
403 fn is_verbose(&self) -> bool;
404}
405
406pub trait HttpAccessor: Send {
408 fn client(&self) -> &reqwest::Client;
409}
410
411pub trait TestExecutor: Send + Sync {
413 fn execute(
414 &self,
415 orch: &Orchestrator,
416 ) -> impl std::future::Future<Output = Result<(), Error>> + Send;
417}
418
419pub struct PhaseTestExecutor;
421
422impl TestExecutor for PhaseTestExecutor {
423 async fn execute(&self, orch: &Orchestrator) -> Result<(), Error> {
424 crate::phases::run_all_phases(orch).await
425 }
426}
427
428impl ConfigAccessor for Orchestrator {
429 fn config(&self) -> &Config {
430 self.cfg()
431 }
432 fn is_verbose(&self) -> bool {
433 self.is_verbose()
434 }
435}
436
437impl HttpAccessor for Orchestrator {
438 fn client(&self) -> &reqwest::Client {
439 &self.client
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::config::{ConfigSource, OutputSource};
447
448 fn orch_from_source(source: &ConfigSource, early_exit: EarlyExitFlags) -> Orchestrator {
449 let config = Config::from_source(source);
450 Orchestrator::from_config(config, early_exit).unwrap()
451 }
452
453 fn default_early_exit() -> EarlyExitFlags {
454 EarlyExitFlags {
455 show_config_path: false,
456 generate_completion: None,
457 history: false,
458 dry_run: false,
459 }
460 }
461
462 #[test]
463 fn test_orchestrator_creation() {
464 let source = ConfigSource::default();
465 let config = Config::from_source(&source);
466 let orch = Orchestrator::from_config(config, default_early_exit());
467 assert!(orch.is_ok());
468 }
469
470 #[test]
471 fn test_is_verbose_default() {
472 let source = ConfigSource::default();
473 let orch = orch_from_source(&source, default_early_exit());
474 assert!(orch.is_verbose());
475 }
476
477 #[test]
478 fn test_is_verbose_quiet() {
479 let source = ConfigSource {
480 output: OutputSource {
481 quiet: Some(true),
482 ..Default::default()
483 },
484 ..Default::default()
485 };
486 let orch = orch_from_source(&source, default_early_exit());
487 assert!(!orch.is_verbose());
488 }
489
490 #[test]
491 fn test_is_simple_mode_default() {
492 let source = ConfigSource::default();
493 let orch = orch_from_source(&source, default_early_exit());
494 assert!(!orch.is_simple_mode());
495 }
496
497 #[test]
498 fn test_is_simple_mode_simple() {
499 let source = ConfigSource {
500 output: OutputSource {
501 simple: Some(true),
502 ..Default::default()
503 },
504 ..Default::default()
505 };
506 let orch = orch_from_source(&source, default_early_exit());
507 assert!(orch.is_simple_mode());
508 }
509
510 #[test]
511 fn test_dry_run_succeeds() {
512 let source = ConfigSource::default();
513 let early_exit = EarlyExitFlags {
514 dry_run: true,
515 ..default_early_exit()
516 };
517 let orch = orch_from_source(&source, early_exit);
518 orch.run_dry_run();
519 }
520
521 #[test]
522 fn test_early_exit_flags_default() {
523 let flags = default_early_exit();
524 assert!(!flags.show_config_path);
525 assert!(flags.generate_completion.is_none());
526 assert!(!flags.history);
527 assert!(!flags.dry_run);
528 }
529
530 #[test]
531 fn test_storage_builder_defaults() {
532 let builder = StorageBuilder::new();
533 let (saver, history) = builder.build();
534 <dyn crate::storage::SaveResult>::save(&*saver, &crate::types::TestResult::default())
535 .unwrap();
536 let _ = <dyn crate::storage::LoadHistory>::load_recent(&*history, 1);
537 }
538
539 #[test]
540 fn test_storage_builder_custom() {
541 let shared = std::sync::Arc::new(crate::storage::MockStorage::new());
542 let saver = shared.clone() as std::sync::Arc<dyn crate::storage::SaveResult + Send + Sync>;
543 let history =
544 shared.clone() as std::sync::Arc<dyn crate::storage::LoadHistory + Send + Sync>;
545
546 let builder = StorageBuilder::new()
547 .with_saver_arc(saver)
548 .with_history_arc(history);
549
550 let (saver, history) = builder.build();
551
552 let result = crate::types::TestResult::default();
553 <dyn crate::storage::SaveResult>::save(&*saver, &result).unwrap();
554 let loaded = <dyn crate::storage::LoadHistory>::load_recent(&*history, 10).unwrap();
555 assert_eq!(loaded.len(), 1);
556 }
557
558 #[test]
559 fn test_orchestrator_exposes_services() {
560 let args = crate::cli::Args::default();
561 let orch = Orchestrator::new(args, None).unwrap();
562 let _services = orch.services();
563 }
564}