1use 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#[derive(Debug, Clone)]
23pub struct VerificationWorkerConfig {
24 pub id: String,
26 pub ssh_host: String,
28 pub identity_file: Option<PathBuf>,
30 pub build_dir: PathBuf,
32}
33
34impl VerificationWorkerConfig {
35 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#[derive(Debug, Clone)]
48pub struct VerificationResult {
49 pub success: bool,
51 pub local_hash: BinaryHashResult,
53 pub remote_hash: BinaryHashResult,
55 pub rsync_up_ms: u64,
57 pub compilation_ms: u64,
59 pub rsync_down_ms: u64,
61 pub total_ms: u64,
63 pub error: Option<String>,
65}
66
67impl VerificationResult {
68 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
78pub struct RemoteCompilationTest {
80 pub worker: VerificationWorkerConfig,
82 pub test_project: PathBuf,
84 pub timeout: Duration,
86 remote_path_suffix: String,
88 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 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 pub fn with_timeout(mut self, timeout: Duration) -> Self {
137 self.timeout = timeout;
138 self
139 }
140
141 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 fn local_binary_path(&self) -> PathBuf {
149 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
655pub 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
673pub 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
691pub 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}