1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Request {
10 Ping,
12 Shutdown,
14 Status,
16 Lookup {
18 cache_key: String,
20 },
21 Store {
23 cache_key: String,
25 artifact: ArtifactData,
27 },
28 SessionStart {
30 client_pid: u32,
32 working_dir: PathBuf,
34 log_file: Option<PathBuf>,
36 track_stats: bool,
38 journal_path: Option<PathBuf>,
40 },
41 Compile {
43 session_id: String,
45 args: Vec<String>,
47 cwd: PathBuf,
49 compiler: PathBuf,
51 env: Option<Vec<(String, String)>>,
55 },
56 SessionEnd {
58 session_id: String,
60 },
61 Clear,
63 CompileEphemeral {
67 client_pid: u32,
69 working_dir: PathBuf,
71 compiler: PathBuf,
73 args: Vec<String>,
75 cwd: PathBuf,
77 env: Option<Vec<(String, String)>>,
79 },
80 LinkEphemeral {
83 client_pid: u32,
85 tool: PathBuf,
87 args: Vec<String>,
89 cwd: PathBuf,
91 env: Option<Vec<(String, String)>>,
93 },
94 SessionStats {
97 session_id: String,
99 },
100 FingerprintCheck {
103 cache_file: PathBuf,
105 cache_type: String,
107 root: PathBuf,
109 extensions: Vec<String>,
112 include_globs: Vec<String>,
115 exclude: Vec<String>,
117 },
118 FingerprintMarkSuccess {
120 cache_file: PathBuf,
122 },
123 FingerprintMarkFailure {
125 cache_file: PathBuf,
127 },
128 FingerprintInvalidate {
130 cache_file: PathBuf,
132 },
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub enum Response {
138 Pong,
140 ShuttingDown,
142 Status(DaemonStatus),
144 LookupResult(LookupResult),
146 StoreResult(StoreResult),
148 SessionStarted {
150 session_id: String,
152 journal_path: Option<PathBuf>,
154 },
155 CompileResult {
157 exit_code: i32,
159 stdout: Arc<Vec<u8>>,
161 stderr: Arc<Vec<u8>>,
163 cached: bool,
165 },
166 SessionEnded {
168 stats: Option<SessionStats>,
170 },
171 LinkResult {
173 exit_code: i32,
175 stdout: Arc<Vec<u8>>,
177 stderr: Arc<Vec<u8>>,
179 cached: bool,
181 warning: Option<String>,
183 },
184 Error {
186 message: String,
188 },
189 Cleared {
191 artifacts_removed: u64,
193 metadata_cleared: u64,
195 dep_graph_contexts_cleared: u64,
197 on_disk_bytes_freed: u64,
199 },
200 SessionStatsResult {
203 stats: Option<SessionStats>,
205 },
206 FingerprintCheckResult {
209 decision: String,
211 reason: Option<String>,
213 changed_files: Vec<String>,
215 },
216 FingerprintAck,
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct DaemonStatus {
223 pub version: String,
225 pub artifact_count: u64,
227 pub cache_size_bytes: u64,
229 pub metadata_entries: u64,
231 pub uptime_secs: u64,
233 pub cache_hits: u64,
235 pub cache_misses: u64,
237 pub total_compilations: u64,
239 pub non_cacheable: u64,
241 pub compile_errors: u64,
243 pub time_saved_ms: u64,
245 pub total_links: u64,
247 pub link_hits: u64,
249 pub link_misses: u64,
251 pub link_non_cacheable: u64,
253 pub dep_graph_contexts: u64,
255 pub dep_graph_files: u64,
257 pub sessions_total: u64,
259 pub sessions_active: u64,
261 pub cache_dir: PathBuf,
263 pub dep_graph_version: u32,
265 pub dep_graph_disk_size: u64,
267}
268
269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub enum LookupResult {
272 Hit {
274 artifact: ArtifactData,
276 },
277 Miss,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub enum StoreResult {
284 Stored,
286 AlreadyExists,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct ArtifactData {
293 pub outputs: Vec<ArtifactOutput>,
295 pub stdout: Arc<Vec<u8>>,
297 pub stderr: Arc<Vec<u8>>,
299 pub exit_code: i32,
301}
302
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct SessionStats {
306 pub duration_ms: u64,
308 pub compilations: u64,
310 pub hits: u64,
312 pub misses: u64,
314 pub non_cacheable: u64,
316 pub errors: u64,
318 pub time_saved_ms: u64,
320 pub unique_sources: u64,
322 pub bytes_read: u64,
324 pub bytes_written: u64,
326}
327
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330pub struct ArtifactOutput {
331 pub name: String,
333 pub data: Arc<Vec<u8>>,
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn roundtrip<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
343 val: &T,
344 ) {
345 let bytes = bincode::serialize(val).unwrap();
346 let decoded: T = bincode::deserialize(&bytes).unwrap();
347 assert_eq!(*val, decoded);
348 }
349
350 #[test]
351 fn session_stats_roundtrip() {
352 let stats = SessionStats {
353 duration_ms: 12345,
354 compilations: 100,
355 hits: 80,
356 misses: 15,
357 non_cacheable: 5,
358 errors: 2,
359 time_saved_ms: 8000,
360 unique_sources: 42,
361 bytes_read: 1024 * 1024,
362 bytes_written: 512 * 1024,
363 };
364 roundtrip(&stats);
365 }
366
367 #[test]
368 fn session_stats_default_zeros() {
369 let stats = SessionStats {
370 duration_ms: 0,
371 compilations: 0,
372 hits: 0,
373 misses: 0,
374 non_cacheable: 0,
375 errors: 0,
376 time_saved_ms: 0,
377 unique_sources: 0,
378 bytes_read: 0,
379 bytes_written: 0,
380 };
381 roundtrip(&stats);
382 }
383
384 #[test]
385 fn daemon_status_expanded_roundtrip() {
386 let status = DaemonStatus {
387 version: env!("CARGO_PKG_VERSION").to_string(),
388 artifact_count: 892,
389 cache_size_bytes: 147_000_000,
390 metadata_entries: 5430,
391 uptime_secs: 8040,
392 cache_hits: 1089,
393 cache_misses: 143,
394 total_compilations: 1247,
395 non_cacheable: 15,
396 compile_errors: 3,
397 time_saved_ms: 750_000,
398 total_links: 50,
399 link_hits: 38,
400 link_misses: 10,
401 link_non_cacheable: 2,
402 dep_graph_contexts: 892,
403 dep_graph_files: 4201,
404 sessions_total: 41,
405 sessions_active: 3,
406 cache_dir: PathBuf::from("/home/user/.zccache"),
407 dep_graph_version: 1,
408 dep_graph_disk_size: 2_500_000,
409 };
410 roundtrip(&status);
411 }
412
413 #[test]
414 fn session_start_with_track_stats_roundtrip() {
415 let req = Request::SessionStart {
416 client_pid: 1234,
417 working_dir: PathBuf::from("/home/user/project"),
418 log_file: None,
419 track_stats: true,
420 journal_path: None,
421 };
422 roundtrip(&req);
423
424 let req_no_stats = Request::SessionStart {
425 client_pid: 1234,
426 working_dir: PathBuf::from("/home/user/project"),
427 log_file: None,
428 track_stats: false,
429 journal_path: None,
430 };
431 roundtrip(&req_no_stats);
432 }
433
434 #[test]
435 fn session_start_with_journal_path_roundtrip() {
436 let req = Request::SessionStart {
437 client_pid: 5678,
438 working_dir: PathBuf::from("/home/user/project"),
439 log_file: None,
440 track_stats: false,
441 journal_path: Some(PathBuf::from("/tmp/build.jsonl")),
442 };
443 roundtrip(&req);
444
445 let req_no_journal = Request::SessionStart {
446 client_pid: 5678,
447 working_dir: PathBuf::from("/home/user/project"),
448 log_file: None,
449 track_stats: false,
450 journal_path: None,
451 };
452 roundtrip(&req_no_journal);
453 }
454
455 #[test]
456 fn session_started_with_journal_path_roundtrip() {
457 let resp = Response::SessionStarted {
458 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
459 journal_path: Some(PathBuf::from(
460 "/home/user/.zccache/logs/sessions/test.jsonl",
461 )),
462 };
463 roundtrip(&resp);
464
465 let resp_no_journal = Response::SessionStarted {
466 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
467 journal_path: None,
468 };
469 roundtrip(&resp_no_journal);
470 }
471
472 #[test]
473 fn session_ended_with_stats_roundtrip() {
474 let stats = SessionStats {
475 duration_ms: 34000,
476 compilations: 32,
477 hits: 28,
478 misses: 3,
479 non_cacheable: 1,
480 errors: 0,
481 time_saved_ms: 8200,
482 unique_sources: 30,
483 bytes_read: 2_000_000,
484 bytes_written: 500_000,
485 };
486 let resp = Response::SessionEnded { stats: Some(stats) };
487 roundtrip(&resp);
488
489 let resp_no_stats = Response::SessionEnded { stats: None };
490 roundtrip(&resp_no_stats);
491 }
492
493 #[test]
494 fn clear_request_roundtrip() {
495 roundtrip(&Request::Clear);
496 }
497
498 #[test]
499 fn cleared_response_roundtrip() {
500 roundtrip(&Response::Cleared {
501 artifacts_removed: 42,
502 metadata_cleared: 100,
503 dep_graph_contexts_cleared: 25,
504 on_disk_bytes_freed: 1024 * 1024,
505 });
506 }
507
508 #[test]
509 fn compile_ephemeral_roundtrip() {
510 roundtrip(&Request::CompileEphemeral {
511 client_pid: 9876,
512 working_dir: PathBuf::from("/home/user/project"),
513 compiler: PathBuf::from("/usr/bin/clang++"),
514 args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
515 cwd: PathBuf::from("/home/user/project/build"),
516 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
517 });
518 roundtrip(&Request::CompileEphemeral {
520 client_pid: 1,
521 working_dir: PathBuf::from("."),
522 compiler: PathBuf::from("gcc"),
523 args: vec![],
524 cwd: PathBuf::from("."),
525 env: None,
526 });
527 }
528
529 #[test]
530 fn link_ephemeral_roundtrip() {
531 roundtrip(&Request::LinkEphemeral {
532 client_pid: 5555,
533 tool: PathBuf::from("/usr/bin/ar"),
534 args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
535 cwd: PathBuf::from("/home/user/project/build"),
536 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
537 });
538 roundtrip(&Request::LinkEphemeral {
539 client_pid: 1,
540 tool: PathBuf::from("lib.exe"),
541 args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
542 cwd: PathBuf::from("."),
543 env: None,
544 });
545 }
546
547 #[test]
548 fn link_result_roundtrip() {
549 roundtrip(&Response::LinkResult {
550 exit_code: 0,
551 stdout: Arc::new(vec![]),
552 stderr: Arc::new(vec![]),
553 cached: true,
554 warning: None,
555 });
556 roundtrip(&Response::LinkResult {
557 exit_code: 0,
558 stdout: Arc::new(vec![]),
559 stderr: Arc::new(b"some warning".to_vec()),
560 cached: false,
561 warning: Some("non-deterministic: missing D flag".into()),
562 });
563 }
564
565 #[test]
566 fn session_stats_request_roundtrip() {
567 roundtrip(&Request::SessionStats {
568 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
569 });
570 }
571
572 #[test]
573 fn session_stats_result_roundtrip() {
574 let stats = SessionStats {
575 duration_ms: 5000,
576 compilations: 10,
577 hits: 7,
578 misses: 2,
579 non_cacheable: 1,
580 errors: 0,
581 time_saved_ms: 3000,
582 unique_sources: 9,
583 bytes_read: 50_000,
584 bytes_written: 20_000,
585 };
586 roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
587 roundtrip(&Response::SessionStatsResult { stats: None });
588 }
589
590 #[test]
591 fn existing_request_variants_still_work() {
592 roundtrip(&Request::Ping);
593 roundtrip(&Request::Shutdown);
594 roundtrip(&Request::Status);
595 roundtrip(&Request::SessionEnd {
596 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
597 });
598 roundtrip(&Request::Compile {
599 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
600 args: vec!["-c".into(), "foo.c".into()],
601 cwd: PathBuf::from("/tmp"),
602 compiler: PathBuf::from("/usr/bin/gcc"),
603 env: None,
604 });
605 }
606
607 #[test]
608 fn existing_response_variants_still_work() {
609 roundtrip(&Response::Pong);
610 roundtrip(&Response::ShuttingDown);
611 roundtrip(&Response::CompileResult {
612 exit_code: 0,
613 stdout: Arc::new(vec![]),
614 stderr: Arc::new(vec![]),
615 cached: true,
616 });
617 roundtrip(&Response::Error {
618 message: "test".into(),
619 });
620 }
621
622 #[test]
623 fn daemon_status_version_field_roundtrips() {
624 let with_version = DaemonStatus {
625 version: "1.2.3".to_string(),
626 artifact_count: 0,
627 cache_size_bytes: 0,
628 metadata_entries: 0,
629 uptime_secs: 0,
630 cache_hits: 0,
631 cache_misses: 0,
632 total_compilations: 0,
633 non_cacheable: 0,
634 compile_errors: 0,
635 time_saved_ms: 0,
636 total_links: 0,
637 link_hits: 0,
638 link_misses: 0,
639 link_non_cacheable: 0,
640 dep_graph_contexts: 0,
641 dep_graph_files: 0,
642 sessions_total: 0,
643 sessions_active: 0,
644 cache_dir: PathBuf::new(),
645 dep_graph_version: 0,
646 dep_graph_disk_size: 0,
647 };
648 roundtrip(&with_version);
649 }
650
651 const _: () = assert!(crate::PROTOCOL_VERSION > 0);
653 const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 5);
655
656 #[test]
657 fn fingerprint_check_roundtrip() {
658 roundtrip(&Request::FingerprintCheck {
659 cache_file: PathBuf::from("/tmp/lint.json"),
660 cache_type: "two-layer".into(),
661 root: PathBuf::from("/home/user/project/src"),
662 extensions: vec!["rs".into(), "toml".into()],
663 include_globs: vec![],
664 exclude: vec![".git".into(), "target".into()],
665 });
666 roundtrip(&Request::FingerprintCheck {
667 cache_file: PathBuf::from("cache.json"),
668 cache_type: "hash".into(),
669 root: PathBuf::from("."),
670 extensions: vec![],
671 include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
672 exclude: vec![],
673 });
674 }
675
676 #[test]
677 fn fingerprint_mark_success_roundtrip() {
678 roundtrip(&Request::FingerprintMarkSuccess {
679 cache_file: PathBuf::from("/tmp/lint.json"),
680 });
681 }
682
683 #[test]
684 fn fingerprint_mark_failure_roundtrip() {
685 roundtrip(&Request::FingerprintMarkFailure {
686 cache_file: PathBuf::from("/tmp/lint.json"),
687 });
688 }
689
690 #[test]
691 fn fingerprint_invalidate_roundtrip() {
692 roundtrip(&Request::FingerprintInvalidate {
693 cache_file: PathBuf::from("/tmp/lint.json"),
694 });
695 }
696
697 #[test]
698 fn fingerprint_check_result_roundtrip() {
699 roundtrip(&Response::FingerprintCheckResult {
700 decision: "skip".into(),
701 reason: None,
702 changed_files: vec![],
703 });
704 roundtrip(&Response::FingerprintCheckResult {
705 decision: "run".into(),
706 reason: Some("content changed".into()),
707 changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
708 });
709 roundtrip(&Response::FingerprintCheckResult {
710 decision: "run".into(),
711 reason: Some("no cache file".into()),
712 changed_files: vec![],
713 });
714 }
715
716 #[test]
717 fn fingerprint_ack_roundtrip() {
718 roundtrip(&Response::FingerprintAck);
719 }
720
721 #[test]
722 fn artifact_clone_shares_payload_via_arc() {
723 let artifact = ArtifactData {
724 outputs: vec![ArtifactOutput {
725 name: "test.o".into(),
726 data: Arc::new(vec![1, 2, 3, 4]),
727 }],
728 stdout: Arc::new(vec![5, 6]),
729 stderr: Arc::new(vec![7, 8]),
730 exit_code: 0,
731 };
732
733 let cloned = artifact.clone();
734
735 assert!(Arc::ptr_eq(
737 &artifact.outputs[0].data,
738 &cloned.outputs[0].data
739 ));
740 assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
741 assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
742 }
743
744 #[test]
745 fn arc_vec_u8_roundtrip_matches_plain_vec() {
746 let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
748 let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
749
750 let plain_bytes = bincode::serialize(&plain).unwrap();
751 let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
752 assert_eq!(
753 plain_bytes, arc_bytes,
754 "Arc<Vec<u8>> must serialize identically to Vec<u8>"
755 );
756
757 let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
759 let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
760 assert_eq!(decoded_plain, plain);
761 assert_eq!(*decoded_arc, plain);
762 }
763}