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            "--delete",
347            "--exclude",
348            "target/",
349            "--exclude",
350            ".git/",
351        ]);
352
353        if let Some(ssh_option) = self.rsync_ssh_option() {
354            cmd.args(["-e", &ssh_option]);
355        }
356
357        cmd.arg(format!("{}/", self.test_project.display()));
358        cmd.arg(&remote_path);
359
360        info!("Running rsync to {}", remote_path);
361        let output = cmd.output().await.context("Failed to execute rsync")?;
362
363        if !output.status.success() {
364            let stderr = String::from_utf8_lossy(&output.stderr);
365            anyhow::bail!("rsync to worker failed: {}", stderr);
366        }
367        Ok(())
368    }
369
370    /// Build the project on the remote worker.
371    async fn build_remote(&self) -> Result<()> {
372        let remote_project = self.remote_project_path();
373        let build_cmd = format!(
374            "cd {} && cargo build --release",
375            shell_escape_path(&remote_project)
376        );
377
378        let mut cmd = Command::new("ssh");
379        for arg in self.ssh_args() {
380            cmd.arg(arg);
381        }
382        cmd.arg(&build_cmd);
383
384        info!(
385            "Running remote build: ssh {} '{}'",
386            self.worker.ssh_host, build_cmd
387        );
388        let output = cmd
389            .output()
390            .await
391            .context("Failed to execute remote build")?;
392
393        if !output.status.success() {
394            let stderr = String::from_utf8_lossy(&output.stderr);
395            anyhow::bail!("Remote build failed: {}", stderr);
396        }
397        Ok(())
398    }
399
400    /// Sync artifacts back from the remote worker.
401    async fn rsync_from_worker(&self) -> Result<()> {
402        let remote_project = self.remote_project_path();
403        let remote_release_dir = remote_project.join("target/release");
404        let remote_release_with_slash = format!("{}/", remote_release_dir.display());
405        let remote_target = format!(
406            "{}:{}",
407            self.worker.ssh_host,
408            shell_escape_path_str(&remote_release_with_slash)
409        );
410
411        let local_target = self.test_project.join("target/release_remote/");
412        std::fs::create_dir_all(&local_target)
413            .context("Failed to create local target directory")?;
414
415        let mut cmd = Command::new("rsync");
416        cmd.args(["-az"]);
417
418        if let Some(ssh_option) = self.rsync_ssh_option() {
419            cmd.args(["-e", &ssh_option]);
420        }
421
422        cmd.arg(&remote_target);
423        cmd.arg(format!("{}/", local_target.display()));
424
425        info!("Running rsync from {}", remote_target);
426        let output = cmd.output().await.context("Failed to execute rsync")?;
427
428        if !output.status.success() {
429            let stderr = String::from_utf8_lossy(&output.stderr);
430            anyhow::bail!("rsync from worker failed: {}", stderr);
431        }
432        Ok(())
433    }
434
435    /// Clean up remote build artifacts.
436    pub async fn cleanup_remote(&self) -> Result<()> {
437        let remote_project = self.remote_project_path();
438        let cleanup_cmd = format!("rm -rf -- {}", shell_escape_path(&remote_project));
439
440        let mut cmd = Command::new("ssh");
441        for arg in self.ssh_args() {
442            cmd.arg(arg);
443        }
444        cmd.arg(&cleanup_cmd);
445
446        info!("Cleaning up remote: {}", cleanup_cmd);
447        let output = cmd
448            .output()
449            .await
450            .context("Failed to execute remote cleanup")?;
451
452        if !output.status.success() {
453            let stderr = String::from_utf8_lossy(&output.stderr);
454            warn!("Remote cleanup failed (non-fatal): {}", stderr);
455        }
456        Ok(())
457    }
458}
459
460#[cfg(test)]
461mod basic_tests {
462    use super::*;
463    use crate::types::{WorkerConfig, WorkerId};
464
465    fn sample_worker_config() -> WorkerConfig {
466        WorkerConfig {
467            id: WorkerId::new("worker-1"),
468            host: "example.com".to_string(),
469            user: "builder".to_string(),
470            identity_file: "/tmp/id_rsa".to_string(),
471            total_slots: 8,
472            priority: 100,
473            tags: Vec::new(),
474        }
475    }
476
477    #[test]
478    fn test_verification_worker_config_from_worker_config() {
479        let worker = sample_worker_config();
480        let build_dir = PathBuf::from("/tmp/rch-build");
481        let verification = VerificationWorkerConfig::from_worker_config(&worker, build_dir.clone());
482
483        assert_eq!(verification.id, "worker-1");
484        assert_eq!(verification.ssh_host, "builder@example.com");
485        assert_eq!(
486            verification.identity_file,
487            Some(PathBuf::from("/tmp/id_rsa"))
488        );
489        assert_eq!(verification.build_dir, build_dir);
490    }
491
492    #[test]
493    fn test_remote_compilation_paths() {
494        let verification = VerificationWorkerConfig {
495            id: "worker-1".to_string(),
496            ssh_host: "builder@example.com".to_string(),
497            identity_file: Some(PathBuf::from("/tmp/id_rsa")),
498            build_dir: PathBuf::from("/tmp/rch-builds"),
499        };
500        let test_project = PathBuf::from("/tmp/test-project");
501        let test = RemoteCompilationTest::new(verification, test_project.clone())
502            .with_remote_path_suffix("run-1");
503
504        assert_eq!(
505            test.local_binary_path(),
506            test_project.join("target/release/test-project")
507        );
508        assert_eq!(
509            test.remote_binary_path(),
510            test_project.join("target/release_remote/test-project")
511        );
512        assert_eq!(
513            test.remote_project_path(),
514            PathBuf::from("/tmp/rch-builds/self_test/test-project-run-1")
515        );
516    }
517
518    #[test]
519    fn test_remote_compilation_paths_are_isolated_by_default() {
520        let verification = VerificationWorkerConfig {
521            id: "worker-1".to_string(),
522            ssh_host: "builder@example.com".to_string(),
523            identity_file: Some(PathBuf::from("/tmp/id_rsa")),
524            build_dir: PathBuf::from("/tmp/rch-builds"),
525        };
526        let test_project = PathBuf::from("/tmp/test-project");
527        let first = RemoteCompilationTest::new(verification.clone(), test_project.clone());
528        let second = RemoteCompilationTest::new(verification, test_project);
529
530        assert_ne!(first.remote_project_path(), second.remote_project_path());
531        let first_path = first.remote_project_path().display().to_string();
532        assert!(
533            first_path.starts_with("/tmp/rch-builds/self_test/test-project-run-"),
534            "remote path should include an isolated run suffix: {first_path}"
535        );
536    }
537
538    #[test]
539    fn test_remote_project_path_sanitizes_project_and_suffix() {
540        let verification = VerificationWorkerConfig {
541            id: "worker-1".to_string(),
542            ssh_host: "builder@example.com".to_string(),
543            identity_file: Some(PathBuf::from("/tmp/id_rsa")),
544            build_dir: PathBuf::from("/tmp/rch-builds"),
545        };
546        let test_project = PathBuf::from("/tmp/project with spaces");
547        let test = RemoteCompilationTest::new(verification, test_project)
548            .with_remote_path_suffix("../attempt 1");
549
550        assert_eq!(
551            test.remote_project_path(),
552            PathBuf::from("/tmp/rch-builds/self_test/project-with-spaces-..-attempt-1")
553        );
554    }
555
556    #[test]
557    fn test_ssh_args_with_identity() {
558        let verification = VerificationWorkerConfig {
559            id: "worker-1".to_string(),
560            ssh_host: "builder@example.com".to_string(),
561            identity_file: Some(PathBuf::from("/tmp/key.pem")),
562            build_dir: PathBuf::from("/tmp/rch-builds"),
563        };
564        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
565        let args = test.ssh_args();
566
567        assert!(args.contains(&"-i".to_string()));
568        assert!(args.contains(&"/tmp/key.pem".to_string()));
569        assert_eq!(args.last(), Some(&"builder@example.com".to_string()));
570    }
571
572    #[test]
573    fn test_ssh_args_without_identity() {
574        let verification = VerificationWorkerConfig {
575            id: "worker-1".to_string(),
576            ssh_host: "builder@example.com".to_string(),
577            identity_file: None,
578            build_dir: PathBuf::from("/tmp/rch-builds"),
579        };
580        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
581        let args = test.ssh_args();
582
583        assert!(!args.contains(&"-i".to_string()));
584        assert_eq!(args.last(), Some(&"builder@example.com".to_string()));
585    }
586
587    #[test]
588    fn test_rsync_ssh_option() {
589        let verification = VerificationWorkerConfig {
590            id: "worker-1".to_string(),
591            ssh_host: "builder@example.com".to_string(),
592            identity_file: Some(PathBuf::from("/tmp/key.pem")),
593            build_dir: PathBuf::from("/tmp/rch-builds"),
594        };
595        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
596
597        assert_eq!(
598            test.rsync_ssh_option(),
599            Some("ssh -o BatchMode=yes -i /tmp/key.pem".to_string())
600        );
601    }
602
603    #[test]
604    fn test_rsync_ssh_option_quotes_identity_with_spaces() {
605        let verification = VerificationWorkerConfig {
606            id: "worker-1".to_string(),
607            ssh_host: "builder@example.com".to_string(),
608            identity_file: Some(PathBuf::from("/tmp/key files/key.pem")),
609            build_dir: PathBuf::from("/tmp/rch-builds"),
610        };
611        let test = RemoteCompilationTest::new(verification, PathBuf::from("/tmp/project"));
612
613        assert_eq!(
614            test.rsync_ssh_option(),
615            Some("ssh -o BatchMode=yes -i '/tmp/key files/key.pem'".to_string())
616        );
617    }
618
619    fn dummy_hash() -> BinaryHashResult {
620        BinaryHashResult {
621            full_hash: "full".to_string(),
622            code_hash: "code".to_string(),
623            text_section_size: 123,
624            is_debug: false,
625        }
626    }
627
628    #[test]
629    fn test_speedup_factor() {
630        let result = VerificationResult {
631            success: true,
632            local_hash: dummy_hash(),
633            remote_hash: dummy_hash(),
634            rsync_up_ms: 10,
635            compilation_ms: 500,
636            rsync_down_ms: 10,
637            total_ms: 520,
638            error: None,
639        };
640
641        assert_eq!(result.speedup_factor(1000), Some(2.0));
642        assert_eq!(result.speedup_factor(0), None);
643
644        let zero_remote = VerificationResult {
645            compilation_ms: 0,
646            ..result
647        };
648        assert_eq!(zero_remote.speedup_factor(1000), None);
649    }
650}
651
652/// Quick verification that tests basic SSH connectivity to a worker.
653pub async fn verify_ssh_connectivity(worker: &VerificationWorkerConfig) -> Result<bool> {
654    let mut cmd = Command::new("ssh");
655    cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]);
656    if let Some(ref identity) = worker.identity_file {
657        cmd.args(["-i", &identity.to_string_lossy()]);
658    }
659    cmd.arg(&worker.ssh_host);
660    cmd.arg("echo ok");
661
662    let output = cmd
663        .output()
664        .await
665        .context("Failed to execute SSH connectivity test")?;
666
667    Ok(output.status.success())
668}
669
670/// Quick verification that rsync is available on the worker.
671pub async fn verify_rsync_available(worker: &VerificationWorkerConfig) -> Result<bool> {
672    let mut cmd = Command::new("ssh");
673    cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]);
674    if let Some(ref identity) = worker.identity_file {
675        cmd.args(["-i", &identity.to_string_lossy()]);
676    }
677    cmd.arg(&worker.ssh_host);
678    cmd.arg("which rsync");
679
680    let output = cmd
681        .output()
682        .await
683        .context("Failed to check rsync availability")?;
684
685    Ok(output.status.success())
686}
687
688/// Quick verification that cargo is available on the worker.
689pub async fn verify_cargo_available(worker: &VerificationWorkerConfig) -> Result<bool> {
690    let mut cmd = Command::new("ssh");
691    cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]);
692    if let Some(ref identity) = worker.identity_file {
693        cmd.args(["-i", &identity.to_string_lossy()]);
694    }
695    cmd.arg(&worker.ssh_host);
696    cmd.arg("which cargo");
697
698    let output = cmd
699        .output()
700        .await
701        .context("Failed to check cargo availability")?;
702
703    Ok(output.status.success())
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709
710    fn init_test_logging() {
711        let _ = tracing_subscriber::fmt()
712            .with_test_writer()
713            .with_max_level(tracing::Level::INFO)
714            .try_init();
715    }
716
717    #[test]
718    fn test_verification_worker_config_creation() {
719        init_test_logging();
720        info!("TEST START: test_verification_worker_config_creation");
721
722        let worker = VerificationWorkerConfig {
723            id: "test-worker".to_string(),
724            ssh_host: "user@192.168.1.100".to_string(),
725            identity_file: Some(PathBuf::from("/home/user/.ssh/id_rsa")),
726            build_dir: PathBuf::from("/tmp/rch_builds"),
727        };
728
729        info!(
730            "INPUT: VerificationWorkerConfig with id={}, host={}",
731            worker.id, worker.ssh_host
732        );
733
734        assert_eq!(worker.id, "test-worker");
735        assert_eq!(worker.ssh_host, "user@192.168.1.100");
736
737        info!("VERIFY: VerificationWorkerConfig created successfully");
738        info!("TEST PASS: test_verification_worker_config_creation");
739    }
740
741    #[test]
742    fn test_remote_compilation_test_creation() {
743        init_test_logging();
744        info!("TEST START: test_remote_compilation_test_creation");
745
746        let worker = VerificationWorkerConfig {
747            id: "test-worker".to_string(),
748            ssh_host: "localhost".to_string(),
749            identity_file: None,
750            build_dir: PathBuf::from("/tmp/rch_builds"),
751        };
752
753        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/test_project"))
754            .with_timeout(Duration::from_secs(60));
755
756        info!("INPUT: RemoteCompilationTest with timeout=60s");
757
758        assert_eq!(test.timeout, Duration::from_secs(60));
759        assert_eq!(test.test_project, PathBuf::from("/tmp/test_project"));
760
761        info!("VERIFY: RemoteCompilationTest created with correct settings");
762        info!("TEST PASS: test_remote_compilation_test_creation");
763    }
764
765    #[test]
766    fn test_verification_result_speedup() {
767        init_test_logging();
768        info!("TEST START: test_verification_result_speedup");
769
770        let result = VerificationResult {
771            success: true,
772            local_hash: BinaryHashResult {
773                full_hash: "abc".to_string(),
774                code_hash: "xyz".to_string(),
775                text_section_size: 1000,
776                is_debug: false,
777            },
778            remote_hash: BinaryHashResult {
779                full_hash: "abc".to_string(),
780                code_hash: "xyz".to_string(),
781                text_section_size: 1000,
782                is_debug: false,
783            },
784            rsync_up_ms: 100,
785            compilation_ms: 5000,
786            rsync_down_ms: 50,
787            total_ms: 5150,
788            error: None,
789        };
790
791        let speedup = result.speedup_factor(10000);
792        info!("INPUT: local=10000ms, remote=5000ms");
793        info!("RESULT: speedup_factor={:?}", speedup);
794
795        assert!(speedup.is_some());
796        assert!((speedup.unwrap() - 2.0).abs() < 0.01);
797
798        info!("VERIFY: Speedup calculated correctly (2x)");
799        info!("TEST PASS: test_verification_result_speedup");
800    }
801
802    #[test]
803    fn test_local_binary_path() {
804        init_test_logging();
805        info!("TEST START: test_local_binary_path");
806
807        let worker = VerificationWorkerConfig {
808            id: "test".to_string(),
809            ssh_host: "localhost".to_string(),
810            identity_file: None,
811            build_dir: PathBuf::from("/tmp"),
812        };
813
814        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/my_project"));
815        let path = test.local_binary_path();
816
817        info!("INPUT: test_project=/tmp/my_project");
818        info!("RESULT: local_binary_path={:?}", path);
819
820        assert_eq!(
821            path,
822            PathBuf::from("/tmp/my_project/target/release/my_project")
823        );
824
825        info!("VERIFY: Local binary path constructed correctly");
826        info!("TEST PASS: test_local_binary_path");
827    }
828
829    #[test]
830    fn test_remote_binary_path() {
831        init_test_logging();
832        info!("TEST START: test_remote_binary_path");
833
834        let worker = VerificationWorkerConfig {
835            id: "test".to_string(),
836            ssh_host: "localhost".to_string(),
837            identity_file: None,
838            build_dir: PathBuf::from("/tmp"),
839        };
840
841        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/my_project"));
842        let path = test.remote_binary_path();
843
844        info!("INPUT: test_project=/tmp/my_project");
845        info!("RESULT: remote_binary_path={:?}", path);
846
847        assert_eq!(
848            path,
849            PathBuf::from("/tmp/my_project/target/release_remote/my_project")
850        );
851
852        info!("VERIFY: Remote binary path constructed correctly");
853        info!("TEST PASS: test_remote_binary_path");
854    }
855
856    #[test]
857    fn test_ssh_args_with_identity() {
858        init_test_logging();
859        info!("TEST START: test_ssh_args_with_identity");
860
861        let worker = VerificationWorkerConfig {
862            id: "test".to_string(),
863            ssh_host: "user@host.example.com".to_string(),
864            identity_file: Some(PathBuf::from("/home/user/.ssh/mykey")),
865            build_dir: PathBuf::from("/tmp"),
866        };
867
868        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/project"));
869        let args = test.ssh_args();
870
871        info!("INPUT: identity_file=/home/user/.ssh/mykey");
872        info!("RESULT: ssh_args={:?}", args);
873
874        assert!(args.contains(&"-i".to_string()));
875        assert!(args.contains(&"/home/user/.ssh/mykey".to_string()));
876        assert!(args.contains(&"user@host.example.com".to_string()));
877
878        info!("VERIFY: SSH args include identity file");
879        info!("TEST PASS: test_ssh_args_with_identity");
880    }
881
882    #[test]
883    fn test_ssh_args_without_identity() {
884        init_test_logging();
885        info!("TEST START: test_ssh_args_without_identity");
886
887        let worker = VerificationWorkerConfig {
888            id: "test".to_string(),
889            ssh_host: "user@host".to_string(),
890            identity_file: None,
891            build_dir: PathBuf::from("/tmp"),
892        };
893
894        let test = RemoteCompilationTest::new(worker, PathBuf::from("/tmp/project"));
895        let args = test.ssh_args();
896
897        info!("INPUT: identity_file=None");
898        info!("RESULT: ssh_args={:?}", args);
899
900        assert!(!args.contains(&"-i".to_string()));
901        assert!(args.contains(&"user@host".to_string()));
902
903        info!("VERIFY: SSH args work without identity file");
904        info!("TEST PASS: test_ssh_args_without_identity");
905    }
906}