zoe_client/system_check/
mod.rs

1//! System Check API for comprehensive client testing
2//!
3//! This module provides a comprehensive system check API that can test various
4//! aspects of the Zoe client functionality including connectivity, storage,
5//! and blob services. The API is designed to be used both from CLI tools
6//! and programmatically via FRB bindings.
7
8use 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/// Level of diagnostic message
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "frb-api", frb)]
34pub enum DiagnosticLevel {
35    Error,
36    Warning,
37}
38
39/// A diagnostic message captured during system check
40#[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/// Overall outcome of system check including test results and diagnostics
48#[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/// Configuration for system check operations
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[cfg_attr(feature = "frb-api", frb(opaque))]
61pub struct SystemCheckConfig {
62    /// Size of test data for blob service tests (in bytes)
63    pub blob_test_size: usize,
64    /// Number of test messages for storage tests
65    pub storage_test_count: u32,
66    /// Timeout for individual test operations
67    pub operation_timeout: Duration,
68    /// Whether to skip blob service tests
69    pub skip_blob_tests: bool,
70    /// Whether to skip storage tests
71    pub skip_storage_tests: bool,
72    /// Whether to skip connectivity tests
73    pub skip_connectivity_tests: bool,
74    /// Whether to run offline tests first (before establishing relay connection)
75    pub run_offline_tests: bool,
76    /// Whether to verify sync after establishing connection
77    pub verify_sync: bool,
78    /// Number of offline messages to create for sync verification
79    pub offline_message_count: u32,
80    /// Size of offline blob data for sync verification
81    pub offline_blob_size: usize,
82}
83
84impl Default for SystemCheckConfig {
85    fn default() -> Self {
86        Self {
87            blob_test_size: 1024 * 1024, // 1MB
88            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, // 64KB for offline tests
97        }
98    }
99}
100
101/// Result of a single system check test
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[cfg_attr(feature = "frb-api", frb)]
104pub enum TestResult {
105    /// Test passed successfully
106    Passed,
107    /// Test failed with error message
108    Failed { error: String },
109    /// Test was skipped
110    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/// Detailed information about a test execution
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "frb-api", frb)]
130pub struct TestInfo {
131    /// Name of the test
132    pub name: String,
133    /// Test result
134    pub result: TestResult,
135    /// Duration of the test execution
136    pub duration: Duration,
137    /// Additional details about the test execution
138    pub details: Vec<String>,
139    /// Timestamp when the test started (as seconds since UNIX epoch)
140    #[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        // Convert to system time for serialization
153        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/// Categories of system check tests
200#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
201#[cfg_attr(feature = "frb-api", frb)]
202pub enum TestCategory {
203    /// Offline storage tests (without relay connection)
204    OfflineStorage,
205    /// Offline blob service tests (without relay connection)
206    OfflineBlob,
207    /// Server connectivity and handshake tests
208    Connectivity,
209    /// Online message storage and retrieval tests
210    Storage,
211    /// Online blob service upload/download tests
212    BlobService,
213    /// Synchronization verification tests
214    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/// Comprehensive results of all system check tests
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[cfg_attr(feature = "frb-api", frb(opaque))]
244pub struct SystemCheckResults {
245    /// Results organized by test category
246    pub results: BTreeMap<TestCategory, Vec<TestInfo>>,
247    /// Overall duration of all tests
248    pub total_duration: Duration,
249    /// Timestamp when the system check started
250    #[serde(with = "instant_serde")]
251    pub started_at: Instant,
252    /// Configuration used for the tests
253    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    /// Add a test result to the specified category
267    pub fn add_test(&mut self, category: TestCategory, test: TestInfo) {
268        self.results.entry(category).or_default().push(test);
269    }
270
271    /// Finalize the results by calculating total duration
272    pub fn finalize(&mut self) {
273        self.total_duration = self.started_at.elapsed();
274    }
275
276    /// Get the overall success status
277    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    /// Get count of passed tests
286    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    /// Get count of failed tests
295    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    /// Get count of skipped tests
304    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    /// Get total count of tests
313    pub fn total_count(&self) -> usize {
314        self.results.values().flat_map(|tests| tests.iter()).count()
315    }
316
317    /// Get the first failed test, if any
318    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    /// Get results for a specific category
326    pub fn get_category_results(&self, category: TestCategory) -> Option<&Vec<TestInfo>> {
327        self.results.get(&category)
328    }
329
330    /// Check if a specific category has any failures
331    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// FRB functions for SystemCheckResults
340#[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/// Main system check runner
395#[cfg_attr(feature = "frb-api", frb(opaque))]
396pub struct SystemCheck {
397    client: Client,
398    config: SystemCheckConfig,
399}
400
401impl SystemCheck {
402    /// Create a new system check instance
403    pub fn new(client: Client, config: SystemCheckConfig) -> Self {
404        Self { client, config }
405    }
406
407    /// Create a new system check instance with default configuration
408    pub fn with_defaults(client: Client) -> Self {
409        Self::new(client, SystemCheckConfig::default())
410    }
411
412    /// Run all enabled system checks in the comprehensive flow:
413    /// 1. Offline tests (if enabled)
414    /// 2. Connectivity tests
415    /// 3. Online tests
416    /// 4. Synchronization verification (if enabled)
417    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            // Phase 1: Offline tests (before establishing relay connection)
432            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            // Phase 2: Connectivity tests (establish relay connection)
457            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            // Phase 3: Online tests (with relay connection)
468            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            // Phase 4: Synchronization verification (verify offline data synced)
489            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    /// Run only connectivity tests
505    pub async fn run_connectivity_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
506        Ok(connectivity::run_tests(&self.client, &self.config).await)
507    }
508
509    /// Run only storage tests
510    pub async fn run_storage_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
511        Ok(storage::run_tests(&self.client, &self.config).await)
512    }
513
514    /// Run only blob service tests
515    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    /// Run only offline storage tests
520    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    /// Run only offline blob tests
525    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    /// Run only synchronization tests
530    pub async fn run_synchronization_tests(&self) -> Result<Vec<TestInfo>, ClientError> {
531        Ok(synchronization::run_tests(&self.client, &self.config).await)
532    }
533
534    /// Run tests for a specific category
535    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    /// Create a new configuration with default values
553    pub fn new() -> Self {
554        Self::default()
555    }
556
557    /// Set the blob test size
558    pub fn with_blob_test_size(mut self, size: usize) -> Self {
559        self.blob_test_size = size;
560        self
561    }
562
563    /// Set the storage test count
564    pub fn with_storage_test_count(mut self, count: u32) -> Self {
565        self.storage_test_count = count;
566        self
567    }
568
569    /// Set the operation timeout
570    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    /// Skip blob service tests
576    pub fn skip_blob_tests(mut self) -> Self {
577        self.skip_blob_tests = true;
578        self
579    }
580
581    /// Skip storage tests
582    pub fn skip_storage_tests(mut self) -> Self {
583        self.skip_storage_tests = true;
584        self
585    }
586
587    /// Skip connectivity tests
588    pub fn skip_connectivity_tests(mut self) -> Self {
589        self.skip_connectivity_tests = true;
590        self
591    }
592
593    /// Enable/disable offline tests
594    pub fn with_offline_tests(mut self, enabled: bool) -> Self {
595        self.run_offline_tests = enabled;
596        self
597    }
598
599    /// Enable/disable sync verification
600    pub fn with_sync_verification(mut self, enabled: bool) -> Self {
601        self.verify_sync = enabled;
602        self
603    }
604
605    /// Set the number of offline messages for sync verification
606    pub fn with_offline_message_count(mut self, count: u32) -> Self {
607        self.offline_message_count = count;
608        self
609    }
610
611    /// Set the size of offline blob data for sync verification
612    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    /// Create a system check instance for FRB API
621    pub fn create(client: Client) -> Self {
622        Self::with_defaults(client)
623    }
624
625    /// Create a system check instance with custom configuration for FRB API
626    pub fn create_with_config(client: Client, config: SystemCheckConfig) -> Self {
627        Self::new(client, config)
628    }
629}
630
631/// CLI-specific collector for actual errors and warnings from tracing
632#[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
670/// Custom tracing layer to capture ERROR and WARN messages
671pub 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        // Only collect ERROR and WARN level events
703        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
719/// Visitor to extract message from tracing events
720struct 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}