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