1use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5use zccache_core::NormalizedPath;
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: NormalizedPath,
34 log_file: Option<NormalizedPath>,
36 track_stats: bool,
38 journal_path: Option<NormalizedPath>,
40 },
41 Compile {
43 session_id: String,
45 args: Vec<String>,
47 cwd: NormalizedPath,
49 compiler: NormalizedPath,
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: NormalizedPath,
71 compiler: NormalizedPath,
73 args: Vec<String>,
75 cwd: NormalizedPath,
77 env: Option<Vec<(String, String)>>,
79 },
80 LinkEphemeral {
83 client_pid: u32,
85 tool: NormalizedPath,
87 args: Vec<String>,
89 cwd: NormalizedPath,
91 env: Option<Vec<(String, String)>>,
93 },
94 SessionStats {
97 session_id: String,
99 },
100 FingerprintCheck {
103 cache_file: NormalizedPath,
105 cache_type: String,
107 root: NormalizedPath,
109 extensions: Vec<String>,
112 include_globs: Vec<String>,
115 exclude: Vec<String>,
117 },
118 FingerprintMarkSuccess {
120 cache_file: NormalizedPath,
122 },
123 FingerprintMarkFailure {
125 cache_file: NormalizedPath,
127 },
128 FingerprintInvalidate {
130 cache_file: NormalizedPath,
132 },
133 ListRustArtifacts,
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub enum Response {
141 Pong,
143 ShuttingDown,
145 Status(DaemonStatus),
147 LookupResult(LookupResult),
149 StoreResult(StoreResult),
151 SessionStarted {
153 session_id: String,
155 journal_path: Option<NormalizedPath>,
157 },
158 CompileResult {
160 exit_code: i32,
162 stdout: Arc<Vec<u8>>,
164 stderr: Arc<Vec<u8>>,
166 cached: bool,
168 },
169 SessionEnded {
171 stats: Option<SessionStats>,
173 },
174 LinkResult {
176 exit_code: i32,
178 stdout: Arc<Vec<u8>>,
180 stderr: Arc<Vec<u8>>,
182 cached: bool,
184 warning: Option<String>,
186 },
187 Error {
189 message: String,
191 },
192 Cleared {
194 artifacts_removed: u64,
196 metadata_cleared: u64,
198 dep_graph_contexts_cleared: u64,
200 on_disk_bytes_freed: u64,
202 },
203 SessionStatsResult {
206 stats: Option<SessionStats>,
208 },
209 FingerprintCheckResult {
212 decision: String,
214 reason: Option<String>,
216 changed_files: Vec<String>,
218 },
219 FingerprintAck,
221 RustArtifactList { artifacts: Vec<RustArtifactInfo> },
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct DaemonStatus {
229 pub version: String,
231 pub artifact_count: u64,
233 pub cache_size_bytes: u64,
235 pub metadata_entries: u64,
237 pub uptime_secs: u64,
239 pub cache_hits: u64,
241 pub cache_misses: u64,
243 pub total_compilations: u64,
245 pub non_cacheable: u64,
247 pub compile_errors: u64,
249 pub time_saved_ms: u64,
251 pub total_links: u64,
253 pub link_hits: u64,
255 pub link_misses: u64,
257 pub link_non_cacheable: u64,
259 pub dep_graph_contexts: u64,
261 pub dep_graph_files: u64,
263 pub sessions_total: u64,
265 pub sessions_active: u64,
267 pub cache_dir: NormalizedPath,
269 pub dep_graph_version: u32,
271 pub dep_graph_disk_size: u64,
273 pub dep_graph_persisted: bool,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub enum LookupResult {
284 Hit {
286 artifact: ArtifactData,
288 },
289 Miss,
291}
292
293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub enum StoreResult {
296 Stored,
298 AlreadyExists,
300}
301
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
304pub struct ArtifactData {
305 pub outputs: Vec<ArtifactOutput>,
307 pub stdout: Arc<Vec<u8>>,
309 pub stderr: Arc<Vec<u8>>,
311 pub exit_code: i32,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
317pub struct SessionStats {
318 pub duration_ms: u64,
320 pub compilations: u64,
322 pub hits: u64,
324 pub misses: u64,
326 pub non_cacheable: u64,
328 pub errors: u64,
330 pub time_saved_ms: u64,
332 pub unique_sources: u64,
334 pub bytes_read: u64,
336 pub bytes_written: u64,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342pub struct ArtifactOutput {
343 pub name: String,
345 pub data: Arc<Vec<u8>>,
347}
348
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub struct RustArtifactInfo {
352 pub cache_key: String,
354 pub output_names: Vec<String>,
356 pub payload_count: usize,
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn roundtrip<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
366 val: &T,
367 ) {
368 let bytes = bincode::serialize(val).unwrap();
369 let decoded: T = bincode::deserialize(&bytes).unwrap();
370 assert_eq!(*val, decoded);
371 }
372
373 #[test]
374 fn session_stats_roundtrip() {
375 let stats = SessionStats {
376 duration_ms: 12345,
377 compilations: 100,
378 hits: 80,
379 misses: 15,
380 non_cacheable: 5,
381 errors: 2,
382 time_saved_ms: 8000,
383 unique_sources: 42,
384 bytes_read: 1024 * 1024,
385 bytes_written: 512 * 1024,
386 };
387 roundtrip(&stats);
388 }
389
390 #[test]
391 fn session_stats_default_zeros() {
392 let stats = SessionStats {
393 duration_ms: 0,
394 compilations: 0,
395 hits: 0,
396 misses: 0,
397 non_cacheable: 0,
398 errors: 0,
399 time_saved_ms: 0,
400 unique_sources: 0,
401 bytes_read: 0,
402 bytes_written: 0,
403 };
404 roundtrip(&stats);
405 }
406
407 #[test]
408 fn daemon_status_expanded_roundtrip() {
409 let status = DaemonStatus {
410 version: env!("CARGO_PKG_VERSION").to_string(),
411 artifact_count: 892,
412 cache_size_bytes: 147_000_000,
413 metadata_entries: 5430,
414 uptime_secs: 8040,
415 cache_hits: 1089,
416 cache_misses: 143,
417 total_compilations: 1247,
418 non_cacheable: 15,
419 compile_errors: 3,
420 time_saved_ms: 750_000,
421 total_links: 50,
422 link_hits: 38,
423 link_misses: 10,
424 link_non_cacheable: 2,
425 dep_graph_contexts: 892,
426 dep_graph_files: 4201,
427 sessions_total: 41,
428 sessions_active: 3,
429 cache_dir: "/home/user/.zccache".into(),
430 dep_graph_version: 1,
431 dep_graph_disk_size: 2_500_000,
432 dep_graph_persisted: true,
433 };
434 roundtrip(&status);
435 }
436
437 #[test]
438 fn session_start_with_track_stats_roundtrip() {
439 let req = Request::SessionStart {
440 client_pid: 1234,
441 working_dir: "/home/user/project".into(),
442 log_file: None,
443 track_stats: true,
444 journal_path: None,
445 };
446 roundtrip(&req);
447
448 let req_no_stats = Request::SessionStart {
449 client_pid: 1234,
450 working_dir: "/home/user/project".into(),
451 log_file: None,
452 track_stats: false,
453 journal_path: None,
454 };
455 roundtrip(&req_no_stats);
456 }
457
458 #[test]
459 fn session_start_with_journal_path_roundtrip() {
460 let req = Request::SessionStart {
461 client_pid: 5678,
462 working_dir: "/home/user/project".into(),
463 log_file: None,
464 track_stats: false,
465 journal_path: Some("/tmp/build.jsonl".into()),
466 };
467 roundtrip(&req);
468
469 let req_no_journal = Request::SessionStart {
470 client_pid: 5678,
471 working_dir: "/home/user/project".into(),
472 log_file: None,
473 track_stats: false,
474 journal_path: None,
475 };
476 roundtrip(&req_no_journal);
477 }
478
479 #[test]
480 fn session_started_with_journal_path_roundtrip() {
481 let resp = Response::SessionStarted {
482 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
483 journal_path: Some("/home/user/.zccache/logs/sessions/test.jsonl".into()),
484 };
485 roundtrip(&resp);
486
487 let resp_no_journal = Response::SessionStarted {
488 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
489 journal_path: None,
490 };
491 roundtrip(&resp_no_journal);
492 }
493
494 #[test]
495 fn session_ended_with_stats_roundtrip() {
496 let stats = SessionStats {
497 duration_ms: 34000,
498 compilations: 32,
499 hits: 28,
500 misses: 3,
501 non_cacheable: 1,
502 errors: 0,
503 time_saved_ms: 8200,
504 unique_sources: 30,
505 bytes_read: 2_000_000,
506 bytes_written: 500_000,
507 };
508 let resp = Response::SessionEnded { stats: Some(stats) };
509 roundtrip(&resp);
510
511 let resp_no_stats = Response::SessionEnded { stats: None };
512 roundtrip(&resp_no_stats);
513 }
514
515 #[test]
516 fn clear_request_roundtrip() {
517 roundtrip(&Request::Clear);
518 }
519
520 #[test]
521 fn cleared_response_roundtrip() {
522 roundtrip(&Response::Cleared {
523 artifacts_removed: 42,
524 metadata_cleared: 100,
525 dep_graph_contexts_cleared: 25,
526 on_disk_bytes_freed: 1024 * 1024,
527 });
528 }
529
530 #[test]
531 fn compile_ephemeral_roundtrip() {
532 roundtrip(&Request::CompileEphemeral {
533 client_pid: 9876,
534 working_dir: "/home/user/project".into(),
535 compiler: "/usr/bin/clang++".into(),
536 args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
537 cwd: "/home/user/project/build".into(),
538 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
539 });
540 roundtrip(&Request::CompileEphemeral {
542 client_pid: 1,
543 working_dir: ".".into(),
544 compiler: "gcc".into(),
545 args: vec![],
546 cwd: ".".into(),
547 env: None,
548 });
549 }
550
551 #[test]
552 fn link_ephemeral_roundtrip() {
553 roundtrip(&Request::LinkEphemeral {
554 client_pid: 5555,
555 tool: "/usr/bin/ar".into(),
556 args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
557 cwd: "/home/user/project/build".into(),
558 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
559 });
560 roundtrip(&Request::LinkEphemeral {
561 client_pid: 1,
562 tool: "lib.exe".into(),
563 args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
564 cwd: ".".into(),
565 env: None,
566 });
567 }
568
569 #[test]
570 fn link_result_roundtrip() {
571 roundtrip(&Response::LinkResult {
572 exit_code: 0,
573 stdout: Arc::new(vec![]),
574 stderr: Arc::new(vec![]),
575 cached: true,
576 warning: None,
577 });
578 roundtrip(&Response::LinkResult {
579 exit_code: 0,
580 stdout: Arc::new(vec![]),
581 stderr: Arc::new(b"some warning".to_vec()),
582 cached: false,
583 warning: Some("non-deterministic: missing D flag".into()),
584 });
585 }
586
587 #[test]
588 fn session_stats_request_roundtrip() {
589 roundtrip(&Request::SessionStats {
590 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
591 });
592 }
593
594 #[test]
595 fn session_stats_result_roundtrip() {
596 let stats = SessionStats {
597 duration_ms: 5000,
598 compilations: 10,
599 hits: 7,
600 misses: 2,
601 non_cacheable: 1,
602 errors: 0,
603 time_saved_ms: 3000,
604 unique_sources: 9,
605 bytes_read: 50_000,
606 bytes_written: 20_000,
607 };
608 roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
609 roundtrip(&Response::SessionStatsResult { stats: None });
610 }
611
612 #[test]
613 fn existing_request_variants_still_work() {
614 roundtrip(&Request::Ping);
615 roundtrip(&Request::Shutdown);
616 roundtrip(&Request::Status);
617 roundtrip(&Request::SessionEnd {
618 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
619 });
620 roundtrip(&Request::Compile {
621 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
622 args: vec!["-c".into(), "foo.c".into()],
623 cwd: "/tmp".into(),
624 compiler: "/usr/bin/gcc".into(),
625 env: None,
626 });
627 }
628
629 #[test]
630 fn existing_response_variants_still_work() {
631 roundtrip(&Response::Pong);
632 roundtrip(&Response::ShuttingDown);
633 roundtrip(&Response::CompileResult {
634 exit_code: 0,
635 stdout: Arc::new(vec![]),
636 stderr: Arc::new(vec![]),
637 cached: true,
638 });
639 roundtrip(&Response::Error {
640 message: "test".into(),
641 });
642 }
643
644 #[test]
645 fn daemon_status_version_field_roundtrips() {
646 let with_version = DaemonStatus {
647 version: "1.2.3".to_string(),
648 artifact_count: 0,
649 cache_size_bytes: 0,
650 metadata_entries: 0,
651 uptime_secs: 0,
652 cache_hits: 0,
653 cache_misses: 0,
654 total_compilations: 0,
655 non_cacheable: 0,
656 compile_errors: 0,
657 time_saved_ms: 0,
658 total_links: 0,
659 link_hits: 0,
660 link_misses: 0,
661 link_non_cacheable: 0,
662 dep_graph_contexts: 0,
663 dep_graph_files: 0,
664 sessions_total: 0,
665 sessions_active: 0,
666 cache_dir: "".into(),
667 dep_graph_version: 0,
668 dep_graph_disk_size: 0,
669 dep_graph_persisted: false,
670 };
671 roundtrip(&with_version);
672 }
673
674 const _: () = assert!(crate::PROTOCOL_VERSION > 0);
676 const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 7);
678
679 #[test]
680 fn fingerprint_check_roundtrip() {
681 roundtrip(&Request::FingerprintCheck {
682 cache_file: "/tmp/lint.json".into(),
683 cache_type: "two-layer".into(),
684 root: "/home/user/project/src".into(),
685 extensions: vec!["rs".into(), "toml".into()],
686 include_globs: vec![],
687 exclude: vec![".git".into(), "target".into()],
688 });
689 roundtrip(&Request::FingerprintCheck {
690 cache_file: "cache.json".into(),
691 cache_type: "hash".into(),
692 root: ".".into(),
693 extensions: vec![],
694 include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
695 exclude: vec![],
696 });
697 }
698
699 #[test]
700 fn fingerprint_mark_success_roundtrip() {
701 roundtrip(&Request::FingerprintMarkSuccess {
702 cache_file: "/tmp/lint.json".into(),
703 });
704 }
705
706 #[test]
707 fn fingerprint_mark_failure_roundtrip() {
708 roundtrip(&Request::FingerprintMarkFailure {
709 cache_file: "/tmp/lint.json".into(),
710 });
711 }
712
713 #[test]
714 fn fingerprint_invalidate_roundtrip() {
715 roundtrip(&Request::FingerprintInvalidate {
716 cache_file: "/tmp/lint.json".into(),
717 });
718 }
719
720 #[test]
721 fn fingerprint_check_result_roundtrip() {
722 roundtrip(&Response::FingerprintCheckResult {
723 decision: "skip".into(),
724 reason: None,
725 changed_files: vec![],
726 });
727 roundtrip(&Response::FingerprintCheckResult {
728 decision: "run".into(),
729 reason: Some("content changed".into()),
730 changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
731 });
732 roundtrip(&Response::FingerprintCheckResult {
733 decision: "run".into(),
734 reason: Some("no cache file".into()),
735 changed_files: vec![],
736 });
737 }
738
739 #[test]
740 fn fingerprint_ack_roundtrip() {
741 roundtrip(&Response::FingerprintAck);
742 }
743
744 #[test]
745 fn list_rust_artifacts_request_roundtrip() {
746 roundtrip(&Request::ListRustArtifacts);
747 }
748
749 #[test]
750 fn rust_artifact_list_response_roundtrip() {
751 roundtrip(&Response::RustArtifactList {
752 artifacts: vec![
753 RustArtifactInfo {
754 cache_key: "abc123def456".into(),
755 output_names: vec![
756 "libfoo-abc123.rlib".into(),
757 "libfoo-abc123.rmeta".into(),
758 "foo-abc123.d".into(),
759 ],
760 payload_count: 3,
761 },
762 RustArtifactInfo {
763 cache_key: "deadbeef".into(),
764 output_names: vec!["libbar-deadbeef.rlib".into()],
765 payload_count: 1,
766 },
767 ],
768 });
769 roundtrip(&Response::RustArtifactList { artifacts: vec![] });
771 }
772
773 #[test]
774 fn rust_artifact_info_roundtrip() {
775 roundtrip(&RustArtifactInfo {
776 cache_key: "0123456789abcdef".into(),
777 output_names: vec!["test.o".into()],
778 payload_count: 1,
779 });
780 }
781
782 #[test]
783 fn artifact_clone_shares_payload_via_arc() {
784 let artifact = ArtifactData {
785 outputs: vec![ArtifactOutput {
786 name: "test.o".into(),
787 data: Arc::new(vec![1, 2, 3, 4]),
788 }],
789 stdout: Arc::new(vec![5, 6]),
790 stderr: Arc::new(vec![7, 8]),
791 exit_code: 0,
792 };
793
794 let cloned = artifact.clone();
795
796 assert!(Arc::ptr_eq(
798 &artifact.outputs[0].data,
799 &cloned.outputs[0].data
800 ));
801 assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
802 assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
803 }
804
805 #[test]
806 fn arc_vec_u8_roundtrip_matches_plain_vec() {
807 let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
809 let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
810
811 let plain_bytes = bincode::serialize(&plain).unwrap();
812 let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
813 assert_eq!(
814 plain_bytes, arc_bytes,
815 "Arc<Vec<u8>> must serialize identically to Vec<u8>"
816 );
817
818 let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
820 let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
821 assert_eq!(decoded_plain, plain);
822 assert_eq!(*decoded_arc, plain);
823 }
824}