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