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