Skip to main content

rch_common/
remote_verification.rs

1//! Remote compilation verification via SSH.
2//!
3//! This module implements SSH-based remote compilation verification that tests
4//! the full RCH pipeline by building code on a remote worker and verifying
5//! the output through binary hash comparison.
6
7use anyhow::{Context, Result};
8use shell_escape::escape;
9use std::borrow::Cow;
10use std::path::{Path, PathBuf};
11use std::time::{Duration, Instant};
12use tokio::process::Command;
13use tracing::{error, info, warn};
14use uuid::Uuid;
15
16use crate::binary_hash::{BinaryHashResult, binaries_equivalent, compute_binary_hash};
17use crate::test_change::{TestChangeGuard, TestCodeChange};
18use crate::types::WorkerConfig;
19
20/// Configuration for remote verification testing.
21/// Extends WorkerConfig with verification-specific settings.
22#[derive(Debug, Clone)]
23pub struct VerificationWorkerConfig {
24    /// Unique identifier for the worker.
25    pub id: String,
26    /// SSH host string (e.g., "user@host" or just "host").
27    pub ssh_host: String,
28    /// SSH identity file path (optional).
29    pub identity_file: Option<PathBuf>,
30    /// Remote directory for builds.
31    pub build_dir: PathBuf,
32}
33
34impl VerificationWorkerConfig {
35    /// Create a verification config from an existing WorkerConfig.
36    pub fn from_worker_config(config: &WorkerConfig, build_dir: PathBuf) -> Self {
37        Self {
38            id: config.id.to_string(),
39            ssh_host: format!("{}@{}", config.user, config.host),
40            identity_file: Some(PathBuf::from(&config.identity_file)),
41            build_dir,
42        }
43    }
44}
45
46/// Result of a remote compilation verification test.
47#[derive(Debug, Clone)]
48pub struct VerificationResult {
49    /// Whether the verification succeeded (hashes match).
50    pub success: bool,
51    /// Hash result from the local build.
52    pub local_hash: BinaryHashResult,
53    /// Hash result from the remote build.
54    pub remote_hash: BinaryHashResult,
55    /// Time spent syncing files to the worker (ms).
56    pub rsync_up_ms: u64,
57    /// Time spent compiling on the worker (ms).
58    pub compilation_ms: u64,
59    /// Time spent syncing artifacts back (ms).
60    pub rsync_down_ms: u64,
61    /// Total test duration (ms).
62    pub total_ms: u64,
63    /// Error message if verification failed.
64    pub error: Option<String>,
65}
66
67impl VerificationResult {
68    /// Get the speedup factor (local time / remote time).
69    /// Returns None if either time is zero.
70    pub fn speedup_factor(&self, local_compilation_ms: u64) -> Option<f64> {
71        if self.compilation_ms == 0 || local_compilation_ms == 0 {
72            return None;
73        }
74        Some(local_compilation_ms as f64 / self.compilation_ms as f64)
75    }
76}
77
78/// Remote compilation test configuration and executor.
79pub struct RemoteCompilationTest {
80    /// Worker configuration.
81    pub worker: VerificationWorkerConfig,
82    /// Path to the test project.
83    pub test_project: PathBuf,
84    /// Timeout for the entire test.
85    pub timeout: Duration,
86    /// Unique suffix appended to the remote project directory for this test run.
87    remote_path_suffix: String,
88    /// Local compilation time for comparison (ms).
89    local_compilation_ms: Option<u64>,
90}
91
92fn sanitize_remote_path_component(component: &str, fallback: &str) -> String {
93    let sanitized = component
94        .chars()
95        .map(|ch| {
96            if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
97                ch
98            } else {
99                '-'
100            }
101        })
102        .collect::<String>();
103    let trimmed = sanitized.trim_matches('-');
104    if trimmed.is_empty() {
105        fallback.to_string()
106    } else {
107        trimmed.to_string()
108    }
109}
110
111fn sanitize_remote_path_suffix(suffix: &str) -> String {
112    sanitize_remote_path_component(suffix, "run")
113}
114
115fn shell_escape_path(path: &Path) -> String {
116    escape(path.to_string_lossy()).into_owned()
117}
118
119fn shell_escape_path_str(path: &str) -> String {
120    escape(Cow::from(path)).into_owned()
121}
122
123impl RemoteCompilationTest {
124    /// Create a new remote compilation test.
125    pub fn new(worker: VerificationWorkerConfig, test_project: PathBuf) -> Self {
126        Self {
127            worker,
128            test_project,
129            timeout: Duration::from_secs(120),
130            remote_path_suffix: format!("run-{}", Uuid::new_v4()),
131            local_compilation_ms: None,
132        }
133    }
134
135    /// Set the timeout for the test.
136    pub fn with_timeout(mut self, timeout: Duration) -> Self {
137        self.timeout = timeout;
138        self
139    }
140
141    /// Set the remote project path suffix.
142    pub fn with_remote_path_suffix(mut self, suffix: impl AsRef<str>) -> Self {
143        self.remote_path_suffix = sanitize_remote_path_suffix(suffix.as_ref());
144        self
145    }
146
147    /// Get the path to the local binary after building.
148    fn local_binary_path(&self) -> PathBuf {
149        // Assuming this is a Rust project with a binary of the same name as the directory
150        let project_name = self
151            .test_project
152            .file_name()
153            .and_then(|n| n.to_str())
154            .unwrap_or("test_project");
155        self.test_project.join("target/release").join(project_name)
156    }
157
158    /// Get the path to the remote binary after syncing back.
159    fn remote_binary_path(&self) -> PathBuf {
160        let project_name = self
161            .test_project
162            .file_name()
163            .and_then(|n| n.to_str())
164            .unwrap_or("test_project");
165        self.test_project
166            .join("target/release_remote")
167            .join(project_name)
168    }
169
170    /// Get the remote project path on the worker.
171    fn remote_project_path(&self) -> PathBuf {
172        let project_name = self
173            .test_project
174            .file_name()
175            .and_then(|n| n.to_str())
176            .map(|n| sanitize_remote_path_component(n, "test_project"))
177            .unwrap_or_else(|| "test_project".to_string());
178        let remote_path_suffix = sanitize_remote_path_suffix(&self.remote_path_suffix);
179        self.worker
180            .build_dir
181            .join("self_test")
182            .join(format!("{project_name}-{remote_path_suffix}"))
183    }
184
185    /// Build SSH args with optional identity file.
186    fn ssh_args(&self) -> Vec<String> {
187        let mut args = vec!["-o".to_string(), "BatchMode=yes".to_string()];
188        if let Some(ref identity) = self.worker.identity_file {
189            args.push("-i".to_string());
190            args.push(identity.to_string_lossy().to_string());
191        }
192        args.push(self.worker.ssh_host.clone());
193        args
194    }
195
196    /// Build rsync args with optional identity file.
197    fn rsync_ssh_option(&self) -> Option<String> {
198        self.worker
199            .identity_file
200            .as_ref()
201            .map(|identity| format!("ssh -o BatchMode=yes -i {}", shell_escape_path(identity)))
202    }
203
204    /// Run the full remote compilation verification test.
205    pub async fn run(&mut self) -> Result<VerificationResult> {
206        let start = Instant::now();
207        info!(
208            "Starting remote compilation verification for worker {}",
209            self.worker.id
210        );
211
212        // 1. Apply test change to make binary unique
213        let change = TestCodeChange::for_main_rs(&self.test_project)
214            .context("Failed to create test code change")?;
215        let guard = TestChangeGuard::new(change).context("Failed to apply test change")?;
216        info!("Applied test change: {}", guard.change_id());
217
218        // 2. Build locally first
219        info!("Building locally for reference hash");
220        let local_build_start = Instant::now();
221        self.build_local().await.context("Local build failed")?;
222        let local_compilation_ms = local_build_start.elapsed().as_millis() as u64;
223        self.local_compilation_ms = Some(local_compilation_ms);
224
225        let local_hash = compute_binary_hash(&self.local_binary_path())
226            .context("Failed to compute local binary hash")?;
227        info!(
228            "Local build complete in {}ms: hash={}",
229            local_compilation_ms,
230            &local_hash.code_hash[..16]
231        );
232
233        // 3. rsync up to worker
234        info!("Syncing source to worker {}", self.worker.id);
235        let rsync_up_start = Instant::now();
236        self.rsync_to_worker()
237            .await
238            .context("Failed to rsync to worker")?;
239        let rsync_up_ms = rsync_up_start.elapsed().as_millis() as u64;
240        info!("Source synced to worker in {}ms", rsync_up_ms);
241
242        // 4. Build on worker
243        info!("Building on remote worker");
244        let compile_start = Instant::now();
245        self.build_remote().await.context("Remote build failed")?;
246        let compilation_ms = compile_start.elapsed().as_millis() as u64;
247        info!("Remote build complete in {}ms", compilation_ms);
248
249        // 5. rsync back
250        info!("Syncing artifacts from worker");
251        let rsync_down_start = Instant::now();
252        self.rsync_from_worker()
253            .await
254            .context("Failed to rsync from worker")?;
255        let rsync_down_ms = rsync_down_start.elapsed().as_millis() as u64;
256        info!("Artifacts synced back in {}ms", rsync_down_ms);
257
258        // 6. Compute remote binary hash
259        let remote_hash = compute_binary_hash(&self.remote_binary_path())
260            .context("Failed to compute remote binary hash")?;
261        info!("Remote binary hash: {}", &remote_hash.code_hash[..16]);
262
263        // 7. Compare
264        let success = binaries_equivalent(&local_hash, &remote_hash);
265        let total_ms = start.elapsed().as_millis() as u64;
266
267        let error = if success {
268            info!("Verification PASSED: local and remote hashes match");
269            None
270        } else {
271            let msg = format!(
272                "Binary hashes do not match: local={}, remote={}",
273                &local_hash.code_hash[..16],
274                &remote_hash.code_hash[..16]
275            );
276            error!("Verification FAILED: {}", msg);
277            Some(msg)
278        };
279
280        // Guard will auto-revert on drop
281        drop(guard);
282
283        Ok(VerificationResult {
284            success,
285            local_hash,
286            remote_hash,
287            rsync_up_ms,
288            compilation_ms,
289            rsync_down_ms,
290            total_ms,
291            error,
292        })
293    }
294
295    /// Build the project locally.
296    async fn build_local(&self) -> Result<()> {
297        info!("Running: cargo build --release in {:?}", self.test_project);
298
299        let output = Command::new("cargo")
300            .args(["build", "--release"])
301            .current_dir(&self.test_project)
302            .env("CARGO_INCREMENTAL", "0")
303            .output()
304            .await
305            .context("Failed to execute cargo build")?;
306
307        if !output.status.success() {
308            let stderr = String::from_utf8_lossy(&output.stderr);
309            anyhow::bail!("Local build failed: {}", stderr);
310        }
311        Ok(())
312    }
313
314    /// Sync source files to the remote worker.
315    async fn rsync_to_worker(&self) -> Result<()> {
316        let remote_project = self.remote_project_path();
317        let escaped_remote_project = shell_escape_path(&remote_project);
318        let mkdir_cmd = format!("mkdir -p -- {escaped_remote_project}");
319
320        let mut mkdir = Command::new("ssh");
321        for arg in self.ssh_args() {
322            mkdir.arg(arg);
323        }
324        mkdir.arg(&mkdir_cmd);
325
326        let output = mkdir
327            .output()
328            .await
329            .context("Failed to create remote project directory")?;
330
331        if !output.status.success() {
332            let stderr = String::from_utf8_lossy(&output.stderr);
333            anyhow::bail!("Remote directory creation failed: {}", stderr);
334        }
335
336        let remote_project_with_slash = format!("{}/", remote_project.display());
337        let remote_path = format!(
338            "{}:{}",
339            self.worker.ssh_host,
340            shell_escape_path_str(&remote_project_with_slash)
341        );
342
343        let mut cmd = Command::new("rsync");
344        cmd.args([
345            "-az",
346            "--no-owner",
347            "--no-group",
348            "--delete",
349            "--exclude",
350            "target/",
351            "--exclude",
352            ".git/",
353        ]);
354
355        if let Some(ssh_option) = self.rsync_ssh_option() {
356            cmd.args(["-e", &ssh_option]);
357        }
358
359        cmd.arg(format!("{}/", self.test_project.display()));
360        cmd.arg(&remote_path);
361
362        info!("Running rsync to {}", remote_path);
363        let output = cmd.output().await.context("Failed to execute rsync")?;
364
365        if !output.status.success() {
366            let stderr = String::from_utf8_lossy(&output.stderr);
367            anyhow::bail!("rsync to worker failed: {}", stderr);
368        }
369        Ok(())
370    }
371
372    /// Build the project on the remote worker.
373    async fn build_remote(&self) -> Result<()> {
374        let remote_project = self.remote_project_path();
375        let build_cmd = format!(
376            "cd {} && cargo build --release",
377            shell_escape_path(&remote_project)
378        );
379
380        let mut cmd = Command::new("ssh");
381        for arg in self.ssh_args() {
382            cmd.arg(arg);
383        }
384        cmd.arg(&build_cmd);
385
386        info!(
387            "Running remote build: ssh {} '{}'",
388            self.worker.ssh_host, build_cmd
389        );
390        let output = cmd
391            .output()
392            .await
393            .context("Failed to execute remote build")?;
394
395        if !output.status.success() {
396            let stderr = String::from_utf8_lossy(&output.stderr);
397            anyhow::bail!("Remote build failed: {}", stderr);
398        }
399        Ok(())
400    }
401
402    /// Sync artifacts back from the remote worker.
403    async fn rsync_from_worker(&self) -> Result<()> {
404        let remote_project = self.remote_project_path();
405        let remote_release_dir = remote_project.join("target/release");
406        let remote_release_with_slash = format!("{}/", remote_release_dir.display());
407        let remote_target = format!(
408            "{}:{}",
409            self.worker.ssh_host,
410            shell_escape_path_str(&remote_release_with_slash)
411        );
412
413        let local_target = self.test_project.join("target/release_remote/");
414        std::fs::create_dir_all(&local_target)
415            .context("Failed to create local target directory")?;
416
417        let mut cmd = Command::new("rsync");
418        cmd.args(["-az", "--no-owner", "--no-group"]);
419
420        if let Some(ssh_option) = self.rsync_ssh_option() {
421            cmd.args(["-e", &ssh_option]);
422        }
423
424        cmd.arg(&remote_target);
425        cmd.arg(format!("{}/", local_target.display()));
426
427        info!("Running rsync from {}", remote_target);
428        let output = cmd.output().await.context("Failed to execute rsync")?;
429
430        if !output.status.success() {
431            let stderr = String::from_utf8_lossy(&output.stderr);
432            anyhow::bail!("rsync from worker failed: {}", stderr);
433        }
434        Ok(())
435    }
436
437    /// Clean up remote build artifacts.
438    pub async fn cleanup_remote(&self) -> Result<()> {
439        let remote_project = self.remote_project_path();
440        let cleanup_cmd = format!("rm -rf -- {}", shell_escape_path(&remote_project));
441
442        let mut cmd = Command::new("ssh");
443        for arg in self.ssh_args() {
444            cmd.arg(arg);
445        }
446        cmd.arg(&cleanup_cmd);
447
448        info!("Cleaning up remote: {}", cleanup_cmd);
449        let output = cmd
450            .output()
451            .await
452            .context("Failed to execute remote cleanup")?;
453
454        if !output.status.success() {
455            let stderr = String::from_utf8_lossy(&output.stderr);
456            warn!("Remote cleanup failed (non-fatal): {}", stderr);
457        }
458        Ok(())
459    }
460}
461
462#[cfg(test)]
463mod basic_tests {
464    use super::*;
465    use crate::types::{WorkerConfig, WorkerId};
466
467    fn sample_worker_config() -> WorkerConfig {
468        WorkerConfig {
469            id: WorkerId::new("worker-1"),
470            host: "example.com".to_string(),
471            user: "builder".to_string(),
472            identity_file: "/tmp/id_rsa".to_string(),
473            total_slots: 8,
474            priority: 100,
475            tags: Vec::new(),
476            tools: Vec::new(),
477        }
478    }
479
480    #[test]
481    fn test_verification_worker_config_from_worker_config() {
482        let worker = sample_worker_config();
483        let build_dir = PathBuf::from("/tmp/rch-build");
484        let verification = VerificationWorkerConfig::from_worker_config(&worker, build_dir.clone());
485
486        assert_eq!(verification.id, "worker-1");
487        assert_eq!(verification.ssh_host, "builder@example.com");
488        assert_eq!(
489            verification.identity_file,
490            Some(PathBuf::from("/tmp/id_rsa"))
491        );
492        assert_eq!(verification.build_dir, build_dir);
493    }
494
495    #[test]
496    fn test_remote_compilation_paths() {
497        let verification = VerificationWorkerConfig {
498            id: "worker-1".to_string(),
499            ssh_host: "builder@example.com".to_string(),
500            identity_file: Some(PathBuf::from("/tmp/id_rsa")),
501            build_dir: PathBuf::from("/tmp/rch-builds"),
502        };
503        let test_project = PathBuf::from("/tmp/test-project");
504        let test = RemoteCompilationTest::new(verification, test_project.clone())
505            .with_remote_path_suffix("run-1");
506
507        assert_eq!(
508            test.local_binary_path(),
509            test_project.join("target/release/test-project")
510        );
511        assert_eq!(
512            test.remote_binary_path(),
513            test_project.join("target/release_remote/test-project")
514        );
515        assert_eq!(
516            test.remote_project_path(),
517            PathBuf::from("/tmp/rch-builds/self_test/test-project-run-1")
518        );
519    }
520
521    #[test]
522    fn test_remote_compilation_paths_are_isolated_by_default() {
523        let verification = VerificationWorkerConfig {
524            id: "worker-1".to_string(),
525            ssh_host: "builder@example.com".to_string(),
526            identity_file: Some(PathBuf::from("/tmp/id_rsa")),
527            build_dir: PathBuf::from("/tmp/rch-builds"),
528        };
529        let test_project = PathBuf::from("/tmp/test-project");
530        let first = RemoteCompilationTest::new(verification.clone(), test_project.clone());
531        let second = RemoteCompilationTest::new(verification, test_project);
532
533        assert_ne!(first.remote_project_path(), second.remote_project_path());
534        let first_path = first.remote_project_path().display().to_string();
535        assert!(
536            first_path.starts_with("/tmp/rch-builds/self_test/test-project-run-"),
537            "remote path should include an isolated run suffix: {first_path}"
538        );
539    }
540
541    #[test]
542    fn test_remote_project_path_sanitizes_project_and_suffix() {
543        let verification = VerificationWorkerConfig {
544            id: "worker-1".to_string(),
545            ssh_host: "builder@example.com".to_string(),
546            identity_file: Some(PathBuf::from("/tmp/id_rsa")),
547            build_dir: PathBuf::from("/tmp/rch-builds"),
548        };
549        let test_project = PathBuf::from("/tmp/project with spaces");
550        let test = RemoteCompilationTest::new(verification, test_project)
551            .with_remote_path_suffix("../attempt 1");
552
553        assert_eq!(
554            test.remote_project_path(),
555            PathBuf::from("/tmp/rch-builds/self_test/project-with-spaces-..-attempt-1")
556        );
557    }
558
559    #[test]
560    fn test_ssh_args_with_identity() {
561        let verification = VerificationWorkerConfig {
562            id: "worker-1".to_string(),
563            ssh_host: "builder@example.com".to_string(),
564            identity_file: Some(PathBuf::from("/tmp/key.pem")),
565            build_dir: PathBuf::from("/tmp/rch-builds"),
566        };
567        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
568        let args = test.ssh_args();
569
570        assert!(args.contains(&"-i".to_string()));
571        assert!(args.contains(&"/tmp/key.pem".to_string()));
572        assert_eq!(args.last(), Some(&"builder@example.com".to_string()));
573    }
574
575    #[test]
576    fn test_ssh_args_without_identity() {
577        let verification = VerificationWorkerConfig {
578            id: "worker-1".to_string(),
579            ssh_host: "builder@example.com".to_string(),
580            identity_file: None,
581            build_dir: PathBuf::from("/tmp/rch-builds"),
582        };
583        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
584        let args = test.ssh_args();
585
586        assert!(!args.contains(&"-i".to_string()));
587        assert_eq!(args.last(), Some(&"builder@example.com".to_string()));
588    }
589
590    #[test]
591    fn test_rsync_ssh_option() {
592        let verification = VerificationWorkerConfig {
593            id: "worker-1".to_string(),
594            ssh_host: "builder@example.com".to_string(),
595            identity_file: Some(PathBuf::from("/tmp/key.pem")),
596            build_dir: PathBuf::from("/tmp/rch-builds"),
597        };
598        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
599
600        assert_eq!(
601            test.rsync_ssh_option(),
602            Some("ssh -o BatchMode=yes -i /tmp/key.pem".to_string())
603        );
604    }
605
606    #[test]
607    fn test_rsync_ssh_option_quotes_identity_with_spaces() {
608        let verification = VerificationWorkerConfig {
609            id: "worker-1".to_string(),
610            ssh_host: "builder@example.com".to_string(),
611            identity_file: Some(PathBuf::from("/tmp/key files/key.pem")),
612            build_dir: PathBuf::from("/tmp/rch-builds"),
613        };
614        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
615
616        assert_eq!(
617            test.rsync_ssh_option(),
618            Some("ssh -o BatchMode=yes -i '/tmp/key files/key.pem'".to_string())
619        );
620    }
621
622    fn dummy_hash() -> BinaryHashResult {
623        BinaryHashResult {
624            full_hash: "full".to_string(),
625            code_hash: "code".to_string(),
626            text_section_size: 123,
627            is_debug: false,
628        }
629    }
630
631    #[test]
632    fn test_speedup_factor() {
633        let result = VerificationResult {
634            success: true,
635            local_hash: dummy_hash(),
636            remote_hash: dummy_hash(),
637            rsync_up_ms: 10,
638            compilation_ms: 500,
639            rsync_down_ms: 10,
640            total_ms: 520,
641            error: None,
642        };
643
644        assert_eq!(result.speedup_factor(1000), Some(2.0));
645        assert_eq!(result.speedup_factor(0), None);
646
647        let zero_remote = VerificationResult {
648            compilation_ms: 0,
649            ..result
650        };
651        assert_eq!(zero_remote.speedup_factor(1000), None);
652    }
653}
654
655/// Quick verification that tests basic SSH connectivity to a worker.
656pub async fn verify_ssh_connectivity(worker: &VerificationWorkerConfig) -> Result<bool> {
657    let mut cmd = Command::new("ssh");
658    cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]);
659    if let Some(ref identity) = worker.identity_file {
660        cmd.args(["-i", &identity.to_string_lossy()]);
661    }
662    cmd.arg(&worker.ssh_host);
663    cmd.arg("echo ok");
664
665    let output = cmd
666        .output()
667        .await
668        .context("Failed to execute SSH connectivity test")?;
669
670    Ok(output.status.success())
671}
672
673/// Quick verification that rsync is available on the worker.
674pub async fn verify_rsync_available(worker: &VerificationWorkerConfig) -> Result<bool> {
675    let mut cmd = Command::new("ssh");
676    cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]);
677    if let Some(ref identity) = worker.identity_file {
678        cmd.args(["-i", &identity.to_string_lossy()]);
679    }
680    cmd.arg(&worker.ssh_host);
681    cmd.arg("which rsync");
682
683    let output = cmd
684        .output()
685        .await
686        .context("Failed to check rsync availability")?;
687
688    Ok(output.status.success())
689}
690
691/// Quick verification that cargo is available on the worker.
692pub async fn verify_cargo_available(worker: &VerificationWorkerConfig) -> Result<bool> {
693    let mut cmd = Command::new("ssh");
694    cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]);
695    if let Some(ref identity) = worker.identity_file {
696        cmd.args(["-i", &identity.to_string_lossy()]);
697    }
698    cmd.arg(&worker.ssh_host);
699    cmd.arg("which cargo");
700
701    let output = cmd
702        .output()
703        .await
704        .context("Failed to check cargo availability")?;
705
706    Ok(output.status.success())
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    fn init_test_logging() {
714        let _ = tracing_subscriber::fmt()
715            .with_test_writer()
716            .with_max_level(tracing::Level::INFO)
717            .try_init();
718    }
719
720    #[test]
721    fn test_verification_worker_config_creation() {
722        init_test_logging();
723        info!("TEST START: test_verification_worker_config_creation");
724
725        let worker = VerificationWorkerConfig {
726            id: "test-worker".to_string(),
727            ssh_host: "user@192.168.1.100".to_string(),
728            identity_file: Some(PathBuf::from("/home/user/.ssh/id_rsa")),
729            build_dir: PathBuf::from("/tmp/rch_builds"),
730        };
731
732        info!(
733            "INPUT: VerificationWorkerConfig with id={}, host={}",
734            worker.id, worker.ssh_host
735        );
736
737        assert_eq!(worker.id, "test-worker");
738        assert_eq!(worker.ssh_host, "user@192.168.1.100");
739
740        info!("VERIFY: VerificationWorkerConfig created successfully");
741        info!("TEST PASS: test_verification_worker_config_creation");
742    }
743
744    #[test]
745    fn test_remote_compilation_test_creation() {
746        init_test_logging();
747        info!("TEST START: test_remote_compilation_test_creation");
748
749        let worker = VerificationWorkerConfig {
750            id: "test-worker".to_string(),
751            ssh_host: "localhost".to_string(),
752            identity_file: None,
753            build_dir: PathBuf::from("/tmp/rch_builds"),
754        };
755
756        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/test_project"))
757            .with_timeout(Duration::from_secs(60));
758
759        info!("INPUT: RemoteCompilationTest with timeout=60s");
760
761        assert_eq!(test.timeout, Duration::from_secs(60));
762        assert_eq!(test.test_project, PathBuf::from("/tmp/test_project"));
763
764        info!("VERIFY: RemoteCompilationTest created with correct settings");
765        info!("TEST PASS: test_remote_compilation_test_creation");
766    }
767
768    #[test]
769    fn test_verification_result_speedup() {
770        init_test_logging();
771        info!("TEST START: test_verification_result_speedup");
772
773        let result = VerificationResult {
774            success: true,
775            local_hash: BinaryHashResult {
776                full_hash: "abc".to_string(),
777                code_hash: "xyz".to_string(),
778                text_section_size: 1000,
779                is_debug: false,
780            },
781            remote_hash: BinaryHashResult {
782                full_hash: "abc".to_string(),
783                code_hash: "xyz".to_string(),
784                text_section_size: 1000,
785                is_debug: false,
786            },
787            rsync_up_ms: 100,
788            compilation_ms: 5000,
789            rsync_down_ms: 50,
790            total_ms: 5150,
791            error: None,
792        };
793
794        let speedup = result.speedup_factor(10000);
795        info!("INPUT: local=10000ms, remote=5000ms");
796        info!("RESULT: speedup_factor={:?}", speedup);
797
798        assert!(speedup.is_some());
799        assert!((speedup.unwrap() - 2.0).abs() < 0.01);
800
801        info!("VERIFY: Speedup calculated correctly (2x)");
802        info!("TEST PASS: test_verification_result_speedup");
803    }
804
805    #[test]
806    fn test_local_binary_path() {
807        init_test_logging();
808        info!("TEST START: test_local_binary_path");
809
810        let worker = VerificationWorkerConfig {
811            id: "test".to_string(),
812            ssh_host: "localhost".to_string(),
813            identity_file: None,
814            build_dir: PathBuf::from("/tmp"),
815        };
816
817        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/my_project"));
818        let path = test.local_binary_path();
819
820        info!("INPUT: test_project=/tmp/my_project");
821        info!("RESULT: local_binary_path={:?}", path);
822
823        assert_eq!(
824            path,
825            PathBuf::from("/tmp/my_project/target/release/my_project")
826        );
827
828        info!("VERIFY: Local binary path constructed correctly");
829        info!("TEST PASS: test_local_binary_path");
830    }
831
832    #[test]
833    fn test_remote_binary_path() {
834        init_test_logging();
835        info!("TEST START: test_remote_binary_path");
836
837        let worker = VerificationWorkerConfig {
838            id: "test".to_string(),
839            ssh_host: "localhost".to_string(),
840            identity_file: None,
841            build_dir: PathBuf::from("/tmp"),
842        };
843
844        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/my_project"));
845        let path = test.remote_binary_path();
846
847        info!("INPUT: test_project=/tmp/my_project");
848        info!("RESULT: remote_binary_path={:?}", path);
849
850        assert_eq!(
851            path,
852            PathBuf::from("/tmp/my_project/target/release_remote/my_project")
853        );
854
855        info!("VERIFY: Remote binary path constructed correctly");
856        info!("TEST PASS: test_remote_binary_path");
857    }
858
859    #[test]
860    fn test_ssh_args_with_identity() {
861        init_test_logging();
862        info!("TEST START: test_ssh_args_with_identity");
863
864        let worker = VerificationWorkerConfig {
865            id: "test".to_string(),
866            ssh_host: "user@host.example.com".to_string(),
867            identity_file: Some(PathBuf::from("/home/user/.ssh/mykey")),
868            build_dir: PathBuf::from("/tmp"),
869        };
870
871        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/project"));
872        let args = test.ssh_args();
873
874        info!("INPUT: identity_file=/home/user/.ssh/mykey");
875        info!("RESULT: ssh_args={:?}", args);
876
877        assert!(args.contains(&"-i".to_string()));
878        assert!(args.contains(&"/home/user/.ssh/mykey".to_string()));
879        assert!(args.contains(&"user@host.example.com".to_string()));
880
881        info!("VERIFY: SSH args include identity file");
882        info!("TEST PASS: test_ssh_args_with_identity");
883    }
884
885    #[test]
886    fn test_ssh_args_without_identity() {
887        init_test_logging();
888        info!("TEST START: test_ssh_args_without_identity");
889
890        let worker = VerificationWorkerConfig {
891            id: "test".to_string(),
892            ssh_host: "user@host".to_string(),
893            identity_file: None,
894            build_dir: PathBuf::from("/tmp"),
895        };
896
897        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/project"));
898        let args = test.ssh_args();
899
900        info!("INPUT: identity_file=None");
901        info!("RESULT: ssh_args={:?}", args);
902
903        assert!(!args.contains(&"-i".to_string()));
904        assert!(args.contains(&"user@host".to_string()));
905
906        info!("VERIFY: SSH args work without identity file");
907        info!("TEST PASS: test_ssh_args_without_identity");
908    }
909}