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}
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<NormalizedPath>,
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: NormalizedPath,
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: "/home/user/.zccache".into(),
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: "/home/user/project".into(),
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: "/home/user/project".into(),
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: "/home/user/project".into(),
439 log_file: None,
440 track_stats: false,
441 journal_path: Some("/tmp/build.jsonl".into()),
442 };
443 roundtrip(&req);
444
445 let req_no_journal = Request::SessionStart {
446 client_pid: 5678,
447 working_dir: "/home/user/project".into(),
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("/home/user/.zccache/logs/sessions/test.jsonl".into()),
460 };
461 roundtrip(&resp);
462
463 let resp_no_journal = Response::SessionStarted {
464 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
465 journal_path: None,
466 };
467 roundtrip(&resp_no_journal);
468 }
469
470 #[test]
471 fn session_ended_with_stats_roundtrip() {
472 let stats = SessionStats {
473 duration_ms: 34000,
474 compilations: 32,
475 hits: 28,
476 misses: 3,
477 non_cacheable: 1,
478 errors: 0,
479 time_saved_ms: 8200,
480 unique_sources: 30,
481 bytes_read: 2_000_000,
482 bytes_written: 500_000,
483 };
484 let resp = Response::SessionEnded { stats: Some(stats) };
485 roundtrip(&resp);
486
487 let resp_no_stats = Response::SessionEnded { stats: None };
488 roundtrip(&resp_no_stats);
489 }
490
491 #[test]
492 fn clear_request_roundtrip() {
493 roundtrip(&Request::Clear);
494 }
495
496 #[test]
497 fn cleared_response_roundtrip() {
498 roundtrip(&Response::Cleared {
499 artifacts_removed: 42,
500 metadata_cleared: 100,
501 dep_graph_contexts_cleared: 25,
502 on_disk_bytes_freed: 1024 * 1024,
503 });
504 }
505
506 #[test]
507 fn compile_ephemeral_roundtrip() {
508 roundtrip(&Request::CompileEphemeral {
509 client_pid: 9876,
510 working_dir: "/home/user/project".into(),
511 compiler: "/usr/bin/clang++".into(),
512 args: vec!["-c".into(), "main.cpp".into(), "-o".into(), "main.o".into()],
513 cwd: "/home/user/project/build".into(),
514 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
515 });
516 roundtrip(&Request::CompileEphemeral {
518 client_pid: 1,
519 working_dir: ".".into(),
520 compiler: "gcc".into(),
521 args: vec![],
522 cwd: ".".into(),
523 env: None,
524 });
525 }
526
527 #[test]
528 fn link_ephemeral_roundtrip() {
529 roundtrip(&Request::LinkEphemeral {
530 client_pid: 5555,
531 tool: "/usr/bin/ar".into(),
532 args: vec!["rcs".into(), "libfoo.a".into(), "a.o".into(), "b.o".into()],
533 cwd: "/home/user/project/build".into(),
534 env: Some(vec![("PATH".into(), "/usr/bin".into())]),
535 });
536 roundtrip(&Request::LinkEphemeral {
537 client_pid: 1,
538 tool: "lib.exe".into(),
539 args: vec!["/OUT:foo.lib".into(), "a.obj".into()],
540 cwd: ".".into(),
541 env: None,
542 });
543 }
544
545 #[test]
546 fn link_result_roundtrip() {
547 roundtrip(&Response::LinkResult {
548 exit_code: 0,
549 stdout: Arc::new(vec![]),
550 stderr: Arc::new(vec![]),
551 cached: true,
552 warning: None,
553 });
554 roundtrip(&Response::LinkResult {
555 exit_code: 0,
556 stdout: Arc::new(vec![]),
557 stderr: Arc::new(b"some warning".to_vec()),
558 cached: false,
559 warning: Some("non-deterministic: missing D flag".into()),
560 });
561 }
562
563 #[test]
564 fn session_stats_request_roundtrip() {
565 roundtrip(&Request::SessionStats {
566 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
567 });
568 }
569
570 #[test]
571 fn session_stats_result_roundtrip() {
572 let stats = SessionStats {
573 duration_ms: 5000,
574 compilations: 10,
575 hits: 7,
576 misses: 2,
577 non_cacheable: 1,
578 errors: 0,
579 time_saved_ms: 3000,
580 unique_sources: 9,
581 bytes_read: 50_000,
582 bytes_written: 20_000,
583 };
584 roundtrip(&Response::SessionStatsResult { stats: Some(stats) });
585 roundtrip(&Response::SessionStatsResult { stats: None });
586 }
587
588 #[test]
589 fn existing_request_variants_still_work() {
590 roundtrip(&Request::Ping);
591 roundtrip(&Request::Shutdown);
592 roundtrip(&Request::Status);
593 roundtrip(&Request::SessionEnd {
594 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
595 });
596 roundtrip(&Request::Compile {
597 session_id: "550e8400-e29b-41d4-a716-446655440000".into(),
598 args: vec!["-c".into(), "foo.c".into()],
599 cwd: "/tmp".into(),
600 compiler: "/usr/bin/gcc".into(),
601 env: None,
602 });
603 }
604
605 #[test]
606 fn existing_response_variants_still_work() {
607 roundtrip(&Response::Pong);
608 roundtrip(&Response::ShuttingDown);
609 roundtrip(&Response::CompileResult {
610 exit_code: 0,
611 stdout: Arc::new(vec![]),
612 stderr: Arc::new(vec![]),
613 cached: true,
614 });
615 roundtrip(&Response::Error {
616 message: "test".into(),
617 });
618 }
619
620 #[test]
621 fn daemon_status_version_field_roundtrips() {
622 let with_version = DaemonStatus {
623 version: "1.2.3".to_string(),
624 artifact_count: 0,
625 cache_size_bytes: 0,
626 metadata_entries: 0,
627 uptime_secs: 0,
628 cache_hits: 0,
629 cache_misses: 0,
630 total_compilations: 0,
631 non_cacheable: 0,
632 compile_errors: 0,
633 time_saved_ms: 0,
634 total_links: 0,
635 link_hits: 0,
636 link_misses: 0,
637 link_non_cacheable: 0,
638 dep_graph_contexts: 0,
639 dep_graph_files: 0,
640 sessions_total: 0,
641 sessions_active: 0,
642 cache_dir: "".into(),
643 dep_graph_version: 0,
644 dep_graph_disk_size: 0,
645 };
646 roundtrip(&with_version);
647 }
648
649 const _: () = assert!(crate::PROTOCOL_VERSION > 0);
651 const _FINGERPRINT_VERSION: () = assert!(crate::PROTOCOL_VERSION == 5);
653
654 #[test]
655 fn fingerprint_check_roundtrip() {
656 roundtrip(&Request::FingerprintCheck {
657 cache_file: "/tmp/lint.json".into(),
658 cache_type: "two-layer".into(),
659 root: "/home/user/project/src".into(),
660 extensions: vec!["rs".into(), "toml".into()],
661 include_globs: vec![],
662 exclude: vec![".git".into(), "target".into()],
663 });
664 roundtrip(&Request::FingerprintCheck {
665 cache_file: "cache.json".into(),
666 cache_type: "hash".into(),
667 root: ".".into(),
668 extensions: vec![],
669 include_globs: vec!["**/*.cpp".into(), "**/*.h".into()],
670 exclude: vec![],
671 });
672 }
673
674 #[test]
675 fn fingerprint_mark_success_roundtrip() {
676 roundtrip(&Request::FingerprintMarkSuccess {
677 cache_file: "/tmp/lint.json".into(),
678 });
679 }
680
681 #[test]
682 fn fingerprint_mark_failure_roundtrip() {
683 roundtrip(&Request::FingerprintMarkFailure {
684 cache_file: "/tmp/lint.json".into(),
685 });
686 }
687
688 #[test]
689 fn fingerprint_invalidate_roundtrip() {
690 roundtrip(&Request::FingerprintInvalidate {
691 cache_file: "/tmp/lint.json".into(),
692 });
693 }
694
695 #[test]
696 fn fingerprint_check_result_roundtrip() {
697 roundtrip(&Response::FingerprintCheckResult {
698 decision: "skip".into(),
699 reason: None,
700 changed_files: vec![],
701 });
702 roundtrip(&Response::FingerprintCheckResult {
703 decision: "run".into(),
704 reason: Some("content changed".into()),
705 changed_files: vec!["src/main.rs".into(), "src/lib.rs".into()],
706 });
707 roundtrip(&Response::FingerprintCheckResult {
708 decision: "run".into(),
709 reason: Some("no cache file".into()),
710 changed_files: vec![],
711 });
712 }
713
714 #[test]
715 fn fingerprint_ack_roundtrip() {
716 roundtrip(&Response::FingerprintAck);
717 }
718
719 #[test]
720 fn artifact_clone_shares_payload_via_arc() {
721 let artifact = ArtifactData {
722 outputs: vec![ArtifactOutput {
723 name: "test.o".into(),
724 data: Arc::new(vec![1, 2, 3, 4]),
725 }],
726 stdout: Arc::new(vec![5, 6]),
727 stderr: Arc::new(vec![7, 8]),
728 exit_code: 0,
729 };
730
731 let cloned = artifact.clone();
732
733 assert!(Arc::ptr_eq(
735 &artifact.outputs[0].data,
736 &cloned.outputs[0].data
737 ));
738 assert!(Arc::ptr_eq(&artifact.stdout, &cloned.stdout));
739 assert!(Arc::ptr_eq(&artifact.stderr, &cloned.stderr));
740 }
741
742 #[test]
743 fn arc_vec_u8_roundtrip_matches_plain_vec() {
744 let plain: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
746 let arc_wrapped: Arc<Vec<u8>> = Arc::new(plain.clone());
747
748 let plain_bytes = bincode::serialize(&plain).unwrap();
749 let arc_bytes = bincode::serialize(&arc_wrapped).unwrap();
750 assert_eq!(
751 plain_bytes, arc_bytes,
752 "Arc<Vec<u8>> must serialize identically to Vec<u8>"
753 );
754
755 let decoded_plain: Vec<u8> = bincode::deserialize(&arc_bytes).unwrap();
757 let decoded_arc: Arc<Vec<u8>> = bincode::deserialize(&plain_bytes).unwrap();
758 assert_eq!(decoded_plain, plain);
759 assert_eq!(*decoded_arc, plain);
760 }
761}