1use crate::{Client, ClientError};
9use async_stream::stream;
10use futures::{Stream, StreamExt};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15use tracing::{Level, info};
16use tracing_subscriber::Layer;
17
18#[cfg(feature = "frb-api")]
19use flutter_rust_bridge::frb;
20
21pub mod blob_service;
22pub mod connectivity;
23pub mod offline_blob;
24pub mod offline_storage;
25pub mod storage;
26pub mod synchronization;
27
28#[cfg(test)]
29mod tests;
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "frb-api", frb)]
34pub enum DiagnosticLevel {
35 Error,
36 Warning,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41#[cfg_attr(feature = "frb-api", frb)]
42pub struct DiagnosticMessage {
43 pub level: DiagnosticLevel,
44 pub message: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[cfg_attr(feature = "frb-api", frb)]
50pub struct SystemCheckOutcome {
51 pub test_results: SystemCheckResults,
52 pub diagnostics: Vec<DiagnosticMessage>,
53 pub success: bool,
54 pub has_errors: bool,
55 pub has_warnings: bool,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60#[cfg_attr(feature = "frb-api", frb(opaque))]
61pub struct SystemCheckConfig {
62 pub blob_test_size: usize,
64 pub storage_test_count: u32,
66 pub operation_timeout: Duration,
68 pub skip_blob_tests: bool,
70 pub skip_storage_tests: bool,
72 pub skip_connectivity_tests: bool,
74 pub run_offline_tests: bool,
76 pub verify_sync: bool,
78 pub offline_message_count: u32,
80 pub offline_blob_size: usize,
82}
83
84impl Default for SystemCheckConfig {
85 fn default() -> Self {
86 Self {
87 blob_test_size: 1024 * 1024, storage_test_count: 3,
89 operation_timeout: Duration::from_secs(30),
90 skip_blob_tests: false,
91 skip_storage_tests: false,
92 skip_connectivity_tests: false,
93 run_offline_tests: true,
94 verify_sync: true,
95 offline_message_count: 2,
96 offline_blob_size: 64 * 1024, }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[cfg_attr(feature = "frb-api", frb)]
104pub enum TestResult {
105 Passed,
107 Failed { error: String },
109 Skipped,
111}
112
113impl TestResult {
114 pub fn is_passed(&self) -> bool {
115 matches!(self, TestResult::Passed)
116 }
117
118 pub fn is_failed(&self) -> bool {
119 matches!(self, TestResult::Failed { .. })
120 }
121
122 pub fn is_skipped(&self) -> bool {
123 matches!(self, TestResult::Skipped)
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "frb-api", frb)]
130pub struct TestInfo {
131 pub name: String,
133 pub result: TestResult,
135 pub duration: Duration,
137 pub details: Vec<String>,
139 #[serde(with = "instant_serde")]
141 pub started_at: Instant,
142}
143
144mod instant_serde {
145 use serde::{Deserialize, Deserializer, Serialize, Serializer};
146 use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
147
148 pub fn serialize<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error>
149 where
150 S: Serializer,
151 {
152 let system_time = SystemTime::now() - instant.elapsed();
154 let duration_since_epoch = system_time
155 .duration_since(UNIX_EPOCH)
156 .unwrap_or(Duration::ZERO);
157 duration_since_epoch.as_secs().serialize(serializer)
158 }
159
160 pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error>
161 where
162 D: Deserializer<'de>,
163 {
164 let secs = u64::deserialize(deserializer)?;
165 let system_time = UNIX_EPOCH + Duration::from_secs(secs);
166 let now = SystemTime::now();
167 let instant_offset = now.duration_since(system_time).unwrap_or(Duration::ZERO);
168 Ok(Instant::now() - instant_offset)
169 }
170}
171
172impl TestInfo {
173 pub fn new(name: impl Into<String>) -> Self {
174 Self {
175 name: name.into(),
176 result: TestResult::Skipped,
177 duration: Duration::ZERO,
178 details: Vec::new(),
179 started_at: Instant::now(),
180 }
181 }
182
183 pub fn with_result(mut self, result: TestResult) -> Self {
184 self.result = result;
185 self.duration = self.started_at.elapsed();
186 self
187 }
188
189 pub fn add_detail(&mut self, detail: impl Into<String>) {
190 self.details.push(detail.into());
191 }
192
193 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
194 self.add_detail(detail);
195 self
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
201#[cfg_attr(feature = "frb-api", frb)]
202pub enum TestCategory {
203 OfflineStorage,
205 OfflineBlob,
207 Connectivity,
209 Storage,
211 BlobService,
213 Synchronization,
215}
216
217impl TestCategory {
218 pub fn name(&self) -> &'static str {
219 match self {
220 TestCategory::OfflineStorage => "Offline Storage",
221 TestCategory::OfflineBlob => "Offline Blob Service",
222 TestCategory::Connectivity => "Server Connectivity",
223 TestCategory::Storage => "Online Storage",
224 TestCategory::BlobService => "Online Blob Service",
225 TestCategory::Synchronization => "Synchronization",
226 }
227 }
228
229 pub fn emoji(&self) -> &'static str {
230 match self {
231 TestCategory::OfflineStorage => "💽",
232 TestCategory::OfflineBlob => "📁",
233 TestCategory::Connectivity => "🚀",
234 TestCategory::Storage => "💾",
235 TestCategory::BlobService => "📦",
236 TestCategory::Synchronization => "🔄",
237 }
238 }
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243#[cfg_attr(feature = "frb-api", frb(opaque))]
244pub struct SystemCheckResults {
245 pub results: BTreeMap<TestCategory, Vec<TestInfo>>,
247 pub total_duration: Duration,
249 #[serde(with = "instant_serde")]
251 pub started_at: Instant,
252 pub config: SystemCheckConfig,
254}
255
256impl SystemCheckResults {
257 pub fn new(config: SystemCheckConfig) -> Self {
258 Self {
259 results: BTreeMap::new(),
260 total_duration: Duration::ZERO,
261 started_at: Instant::now(),
262 config,
263 }
264 }
265
266 pub fn add_test(&mut self, category: TestCategory, test: TestInfo) {
268 self.results.entry(category).or_default().push(test);
269 }
270
271 pub fn finalize(&mut self) {
273 self.total_duration = self.started_at.elapsed();
274 }
275
276 pub fn is_success(&self) -> bool {
278 self.results.values().all(|tests| {
279 tests
280 .iter()
281 .all(|test| test.result.is_passed() || test.result.is_skipped())
282 })
283 }
284
285 pub fn passed_count(&self) -> usize {
287 self.results
288 .values()
289 .flat_map(|tests| tests.iter())
290 .filter(|test| test.result.is_passed())
291 .count()
292 }
293
294 pub fn failed_count(&self) -> usize {
296 self.results
297 .values()
298 .flat_map(|tests| tests.iter())
299 .filter(|test| test.result.is_failed())
300 .count()
301 }
302
303 pub fn skipped_count(&self) -> usize {
305 self.results
306 .values()
307 .flat_map(|tests| tests.iter())
308 .filter(|test| test.result.is_skipped())
309 .count()
310 }
311
312 pub fn total_count(&self) -> usize {
314 self.results.values().flat_map(|tests| tests.iter()).count()
315 }
316
317 pub fn first_failure(&self) -> Option<&TestInfo> {
319 self.results
320 .values()
321 .flat_map(|tests| tests.iter())
322 .find(|test| test.result.is_failed())
323 }
324
325 pub fn get_category_results(&self, category: TestCategory) -> Option<&Vec<TestInfo>> {
327 self.results.get(&category)
328 }
329
330 pub fn category_has_failures(&self, category: TestCategory) -> bool {
332 self.results
333 .get(&category)
334 .map(|tests| tests.iter().any(|test| test.result.is_failed()))
335 .unwrap_or(false)
336 }
337}
338
339#[cfg(feature = "frb-api")]
341#[frb]
342pub fn system_check_results_is_success(results: &SystemCheckResults) -> bool {
343 results.is_success()
344}
345
346#[cfg(feature = "frb-api")]
347#[frb]
348pub fn system_check_results_passed_count(results: &SystemCheckResults) -> u32 {
349 results.passed_count() as u32
350}
351
352#[cfg(feature = "frb-api")]
353#[frb]
354pub fn system_check_results_failed_count(results: &SystemCheckResults) -> u32 {
355 results.failed_count() as u32
356}
357
358#[cfg(feature = "frb-api")]
359#[frb]
360pub fn system_check_results_total_count(results: &SystemCheckResults) -> u32 {
361 results.total_count() as u32
362}
363
364#[cfg(feature = "frb-api")]
365#[frb]
366pub fn system_check_results_total_duration_ms(results: &SystemCheckResults) -> u64 {
367 results.total_duration.as_millis() as u64
368}
369
370#[cfg(feature = "frb-api")]
371#[frb]
372pub fn system_check_results_get_categories(results: &SystemCheckResults) -> Vec<TestCategory> {
373 results.results.keys().cloned().collect()
374}
375
376#[cfg(feature = "frb-api")]
377#[frb]
378pub fn system_check_results_get_tests_for_category(
379 results: &SystemCheckResults,
380 category: TestCategory,
381) -> Vec<TestInfo> {
382 results.results.get(&category).cloned().unwrap_or_default()
383}
384
385#[cfg(feature = "frb-api")]
386#[frb]
387pub fn system_check_results_category_has_failures(
388 results: &SystemCheckResults,
389 category: TestCategory,
390) -> bool {
391 results.category_has_failures(category)
392}
393
394#[cfg_attr(feature = "frb-api", frb(opaque))]
396pub struct SystemCheck {
397 client: Client,
398 config: SystemCheckConfig,
399}
400
401impl SystemCheck {
402 pub fn new(client: Client, config: SystemCheckConfig) -> Self {
404 Self { client, config }
405 }
406
407 pub fn with_defaults(client: Client) -> Self {
409 Self::new(client, SystemCheckConfig::default())
410 }
411
412 pub async fn run_all(&self) -> Result<SystemCheckResults, ClientError> {
418 self.run_all_stream()
419 .collect::<Vec<_>>()
420 .await
421 .last()
422 .cloned()
423 .ok_or(ClientError::Generic("No results returned".to_string()))
424 }
425
426 pub fn run_all_stream(&self) -> impl Stream<Item = SystemCheckResults> {
427 stream! {
428 let mut results = SystemCheckResults::new(self.config.clone());
429 yield results.clone();
430
431 if self.config.run_offline_tests {
433 info!("🔧 Phase 1: Running offline tests...");
434
435 if !self.config.skip_storage_tests {
436 let offline_storage_results =
437 offline_storage::run_tests(&self.client, &self.config).await;
438 for test in offline_storage_results {
439 results.add_test(TestCategory::OfflineStorage, test);
440 yield results.clone();
441 }
442 yield results.clone();
443 }
444
445 if !self.config.skip_blob_tests {
446 let offline_blob_results =
447 offline_blob::run_tests(&self.client, &self.config).await;
448 for test in offline_blob_results {
449 results.add_test(TestCategory::OfflineBlob, test);
450 yield results.clone();
451 }
452 yield results.clone();
453 }
454 }
455
456 if !self.config.skip_connectivity_tests {
458 info!("🚀 Phase 2: Establishing connectivity...");
459 let connectivity_results = connectivity::run_tests(&self.client, &self.config).await;
460 for test in connectivity_results {
461 results.add_test(TestCategory::Connectivity, test);
462 yield results.clone();
463 }
464 yield results.clone();
465 }
466
467 info!("🌐 Phase 3: Running online tests...");
469
470 if !self.config.skip_storage_tests {
471 let storage_results = storage::run_tests(&self.client, &self.config).await;
472 for test in storage_results {
473 results.add_test(TestCategory::Storage, test);
474 yield results.clone();
475 }
476 yield results.clone();
477 }
478
479 if !self.config.skip_blob_tests {
480 let blob_results = blob_service::run_tests(&self.client, &self.config).await;
481 for test in blob_results {
482 results.add_test(TestCategory::BlobService, test);
483 yield results.clone();
484 }
485 yield results.clone();
486 }
487
488 if self.config.verify_sync && self.config.run_offline_tests {
490 info!("🔄 Phase 4: Verifying synchronization...");
491 let sync_results = synchronization::run_tests(&self.client, &self.config).await;
492 for test in sync_results {
493 results.add_test(TestCategory::Synchronization, test);
494 yield results.clone();
495 }
496 yield results.clone();
497 }
498
499 results.finalize();
500 yield results;
501 }
502 }
503
504 pub async fn run_connectivity_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
506 Ok(connectivity::run_tests(&self.client, &self.config).await)
507 }
508
509 pub async fn run_storage_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
511 Ok(storage::run_tests(&self.client, &self.config).await)
512 }
513
514 pub async fn run_blob_service_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
516 Ok(blob_service::run_tests(&self.client, &self.config).await)
517 }
518
519 pub async fn run_offline_storage_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
521 Ok(offline_storage::run_tests(&self.client, &self.config).await)
522 }
523
524 pub async fn run_offline_blob_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
526 Ok(offline_blob::run_tests(&self.client, &self.config).await)
527 }
528
529 pub async fn run_synchronization_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
531 Ok(synchronization::run_tests(&self.client, &self.config).await)
532 }
533
534 pub async fn run_category_tests(
536 &self,
537 category: TestCategory,
538 ) -> Result<Vec<TestInfo>, ClientError> {
539 match category {
540 TestCategory::OfflineStorage => self.run_offline_storage_tests().await,
541 TestCategory::OfflineBlob => self.run_offline_blob_tests().await,
542 TestCategory::Connectivity => self.run_connectivity_tests().await,
543 TestCategory::Storage => self.run_storage_tests().await,
544 TestCategory::BlobService => self.run_blob_service_tests().await,
545 TestCategory::Synchronization => self.run_synchronization_tests().await,
546 }
547 }
548}
549
550#[cfg_attr(feature = "frb-api", frb)]
551impl SystemCheckConfig {
552 pub fn new() -> Self {
554 Self::default()
555 }
556
557 pub fn with_blob_test_size(mut self, size: usize) -> Self {
559 self.blob_test_size = size;
560 self
561 }
562
563 pub fn with_storage_test_count(mut self, count: u32) -> Self {
565 self.storage_test_count = count;
566 self
567 }
568
569 pub fn with_timeout_secs(mut self, timeout_secs: u64) -> Self {
571 self.operation_timeout = Duration::from_secs(timeout_secs);
572 self
573 }
574
575 pub fn skip_blob_tests(mut self) -> Self {
577 self.skip_blob_tests = true;
578 self
579 }
580
581 pub fn skip_storage_tests(mut self) -> Self {
583 self.skip_storage_tests = true;
584 self
585 }
586
587 pub fn skip_connectivity_tests(mut self) -> Self {
589 self.skip_connectivity_tests = true;
590 self
591 }
592
593 pub fn with_offline_tests(mut self, enabled: bool) -> Self {
595 self.run_offline_tests = enabled;
596 self
597 }
598
599 pub fn with_sync_verification(mut self, enabled: bool) -> Self {
601 self.verify_sync = enabled;
602 self
603 }
604
605 pub fn with_offline_message_count(mut self, count: u32) -> Self {
607 self.offline_message_count = count;
608 self
609 }
610
611 pub fn with_offline_blob_size(mut self, size: usize) -> Self {
613 self.offline_blob_size = size;
614 self
615 }
616}
617
618#[cfg_attr(feature = "frb-api", frb)]
619impl SystemCheck {
620 pub fn create(client: Client) -> Self {
622 Self::with_defaults(client)
623 }
624
625 pub fn create_with_config(client: Client, config: SystemCheckConfig) -> Self {
627 Self::new(client, config)
628 }
629}
630
631#[derive(Debug, Clone, Default)]
633pub struct DiagnosticCollector {
634 errors: Vec<String>,
635 warnings: Vec<String>,
636}
637
638impl DiagnosticCollector {
639 pub fn new() -> Self {
640 Self::default()
641 }
642
643 pub fn has_errors(&self) -> bool {
644 !self.errors.is_empty()
645 }
646
647 pub fn has_warnings(&self) -> bool {
648 !self.warnings.is_empty()
649 }
650
651 pub fn errors(&self) -> &Vec<String> {
652 &self.errors
653 }
654
655 pub fn warnings(&self) -> &Vec<String> {
656 &self.warnings
657 }
658}
659
660impl DiagnosticCollector {
661 fn add_error(&mut self, message: String) {
662 self.errors.push(message);
663 }
664
665 fn add_warning(&mut self, message: String) {
666 self.warnings.push(message);
667 }
668}
669
670pub enum DiagnosticLayer {
672 WithCollector(Arc<Mutex<DiagnosticCollector>>),
673 None,
674}
675
676impl DiagnosticLayer {
677 pub fn new(collector: Arc<Mutex<DiagnosticCollector>>) -> Self {
678 Self::WithCollector(collector)
679 }
680
681 pub fn new_none() -> Self {
682 Self::None
683 }
684}
685
686impl<S> Layer<S> for DiagnosticLayer
687where
688 S: tracing::Subscriber,
689{
690 fn on_event(
691 &self,
692 event: &tracing::Event<'_>,
693 _ctx: tracing_subscriber::layer::Context<'_, S>,
694 ) {
695 let collector_arc = match self {
696 Self::WithCollector(collector) => collector,
697 Self::None => return,
698 };
699
700 let level = *event.metadata().level();
701
702 if level == Level::ERROR || level == Level::WARN {
704 let mut visitor = MessageVisitor::new();
705 event.record(&mut visitor);
706
707 if let Some(message) = visitor.message {
708 let mut collector = collector_arc.lock().unwrap();
709 match level {
710 Level::ERROR => collector.add_error(message),
711 Level::WARN => collector.add_warning(message),
712 _ => {}
713 }
714 }
715 }
716 }
717}
718
719struct MessageVisitor {
721 message: Option<String>,
722}
723
724impl MessageVisitor {
725 fn new() -> Self {
726 Self { message: None }
727 }
728}
729
730impl tracing::field::Visit for MessageVisitor {
731 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
732 if field.name() == "message" {
733 self.message = Some(format!("{value:?}"));
734 }
735 }
736
737 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
738 if field.name() == "message" {
739 self.message = Some(value.to_string());
740 }
741 }
742}