1
+use super::*;
2
+#[cfg(target_os = "linux")]
3
+use crate::protocol::PROFILE_SHM_FUTEX;
4
+use crate::protocol::{increment_encode, BatchBuilder, CgroupsBuilder, PROFILE_BASELINE};
5
+use std::os::fd::RawFd;
6
+use std::os::unix::ffi::OsStrExt;
7
+use std::path::PathBuf;
8
+use std::thread;
9
+use std::time::Duration;
10
+
11
+const TEST_RUN_DIR: &str = "/tmp/nipc_svc_rust_test";
12
+const AUTH_TOKEN: u64 = 0xDEADBEEFCAFEBABE;
13
+const RESPONSE_BUF_SIZE: usize = 65536;
14
+static RAW_SERVICE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
15
+
16
+fn ensure_run_dir() {
17
+ let _ = std::fs::create_dir_all(TEST_RUN_DIR);
18
+}
19
+
20
+fn cleanup_all(service: &str) {
21
+ let _ = std::fs::remove_file(format!("{TEST_RUN_DIR}/{service}.sock"));
22
+ #[cfg(target_os = "linux")]
23
+ crate::transport::shm::cleanup_stale(TEST_RUN_DIR, service);
24
+}
25
+
26
+fn socket_path(service: &str) -> PathBuf {
27
+ PathBuf::from(format!("{TEST_RUN_DIR}/{service}.sock"))
28
+}
29
+
30
+fn unique_service(prefix: &str) -> String {
31
+ format!(
32
+ "{}_{}_{}",
33
+ prefix,
34
+ std::process::id(),
35
+ RAW_SERVICE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1
36
+ )
37
+}
38
+
39
+fn wait_for_listener_bind(service: &str) {
40
+ let sock = socket_path(service);
41
+ for _ in 0..2000 {
42
+ if sock.exists() {
43
+ return;
44
+ }
45
+ thread::sleep(Duration::from_micros(500));
46
+ }
47
+
48
+ panic!("listener did not bind for service {service}");
49
+}
50
+
51
+fn server_config() -> ServerConfig {
52
+ ServerConfig {
53
+ supported_profiles: PROFILE_BASELINE,
54
+ max_request_payload_bytes: 4096,
55
+ max_request_batch_items: 1,
56
+ max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
57
+ max_response_batch_items: 1,
58
+ auth_token: AUTH_TOKEN,
59
+ backlog: 4,
60
+ ..ServerConfig::default()
61
+ }
62
+}
63
+
64
+fn client_config() -> ClientConfig {
65
+ ClientConfig {
66
+ supported_profiles: PROFILE_BASELINE,
67
+ max_request_payload_bytes: 4096,
68
+ max_request_batch_items: 1,
69
+ max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
70
+ max_response_batch_items: 1,
71
+ auth_token: AUTH_TOKEN,
72
+ ..ClientConfig::default()
73
+ }
74
+}
75
+
76
+#[cfg(target_os = "linux")]
77
+fn shm_server_config() -> ServerConfig {
78
+ ServerConfig {
79
+ supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
80
+ preferred_profiles: PROFILE_SHM_FUTEX,
81
+ max_request_payload_bytes: 4096,
82
+ max_request_batch_items: 16,
83
+ max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
84
+ max_response_batch_items: 16,
85
+ auth_token: AUTH_TOKEN,
86
+ backlog: 4,
87
+ ..ServerConfig::default()
88
+ }
89
+}
90
+
91
+#[cfg(target_os = "linux")]
92
+fn shm_client_config() -> ClientConfig {
93
+ ClientConfig {
94
+ supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
95
+ preferred_profiles: PROFILE_SHM_FUTEX,
96
+ max_request_payload_bytes: 4096,
97
+ max_request_batch_items: 16,
98
+ max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
99
+ max_response_batch_items: 16,
100
+ auth_token: AUTH_TOKEN,
101
+ ..ClientConfig::default()
102
+ }
103
+}
104
+
105
+fn batch_server_config() -> ServerConfig {
106
+ ServerConfig {
107
+ supported_profiles: PROFILE_BASELINE,
108
+ max_request_payload_bytes: 4096,
109
+ max_request_batch_items: 16,
110
+ max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
111
+ max_response_batch_items: 16,
112
+ auth_token: AUTH_TOKEN,
113
+ backlog: 4,
114
+ ..ServerConfig::default()
115
+ }
116
+}
117
+
118
+fn batch_client_config() -> ClientConfig {
119
+ ClientConfig {
120
+ supported_profiles: PROFILE_BASELINE,
121
+ max_request_payload_bytes: 4096,
122
+ max_request_batch_items: 16,
123
+ max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
124
+ max_response_batch_items: 16,
125
+ auth_token: AUTH_TOKEN,
126
+ ..ClientConfig::default()
127
+ }
128
+}
129
+
130
+fn snapshot_client(service: &str, config: ClientConfig) -> RawClient {
131
+ RawClient::new_snapshot(TEST_RUN_DIR, service, config)
132
+}
133
+
134
+fn increment_client(service: &str, config: ClientConfig) -> RawClient {
135
+ RawClient::new_increment(TEST_RUN_DIR, service, config)
136
+}
137
+
138
+fn string_reverse_client(service: &str, config: ClientConfig) -> RawClient {
139
+ RawClient::new_string_reverse(TEST_RUN_DIR, service, config)
140
+}
141
+
142
+fn connect_ready(client: &mut RawClient) {
143
+ for _ in 0..200 {
144
+ client.refresh();
145
+ if client.ready() {
146
+ return;
147
+ }
148
+ thread::sleep(Duration::from_millis(10));
149
+ }
150
+
151
+ panic!("client did not reach READY state");
152
+}
153
+
154
+fn fill_test_cgroups_snapshot(builder: &mut CgroupsBuilder<'_>) -> bool {
155
+ let items = [
156
+ (
157
+ 1001u32,
158
+ 0u32,
159
+ 1u32,
160
+ b"docker-abc123" as &[u8],
161
+ b"/sys/fs/cgroup/docker/abc123" as &[u8],
162
+ ),
163
+ (2002, 0, 1, b"k8s-pod-xyz", b"/sys/fs/cgroup/kubepods/xyz"),
164
+ (
165
+ 3003,
166
+ 0,
167
+ 0,
168
+ b"systemd-user",
169
+ b"/sys/fs/cgroup/user.slice/user-1000",
170
+ ),
171
+ ];
172
+
173
+ for (hash, options, enabled, name, path) in &items {
174
+ if builder.add(*hash, *options, *enabled, name, path).is_err() {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ true
180
+}
181
+
182
+fn panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
183
+ match payload.downcast::<String>() {
184
+ Ok(msg) => *msg,
185
+ Err(payload) => match payload.downcast::<&'static str>() {
186
+ Ok(msg) => (*msg).to_string(),
187
+ Err(_) => "<non-string panic>".to_string(),
188
+ },
189
+ }
190
+}
191
+
192
+fn send_raw_packet(fd: i32, data: &[u8]) {
193
+ let sent = unsafe { libc::send(fd, data.as_ptr() as *const libc::c_void, data.len(), 0) };
194
+ assert_eq!(
195
+ sent,
196
+ data.len() as isize,
197
+ "raw send failed: {:?}",
198
+ std::io::Error::last_os_error()
199
+ );
200
+}
201
+
202
+fn build_increment_request_message(message_id: u64, value: u64) -> Vec<u8> {
203
+ let mut payload = [0u8; INCREMENT_PAYLOAD_SIZE];
204
+ let payload_len = increment_encode(value, &mut payload);
205
+ assert_eq!(
206
+ payload_len, INCREMENT_PAYLOAD_SIZE,
207
+ "increment_encode should fit the fixed-size request buffer"
208
+ );
209
+
210
+ let hdr = Header {
211
+ magic: MAGIC_MSG,
212
+ version: VERSION,
213
+ header_len: protocol::HEADER_LEN,
214
+ kind: KIND_REQUEST,
215
+ code: METHOD_INCREMENT,
216
+ flags: 0,
217
+ payload_len: payload_len as u32,
218
+ item_count: 1,
219
+ message_id,
220
+ transport_status: STATUS_OK,
221
+ };
222
+
223
+ let mut msg = vec![0u8; HEADER_SIZE + payload_len];
224
+ hdr.encode(&mut msg[..HEADER_SIZE]);
225
+ msg[HEADER_SIZE..].copy_from_slice(&payload[..payload_len]);
226
+ msg
227
+}
228
+
229
+fn verify_increment_service_ok(service: &str, config: ClientConfig) {
230
+ let mut verify = increment_client(service, config);
231
+ connect_ready(&mut verify);
232
+ assert_eq!(
233
+ verify.call_increment(1).expect("verification call"),
234
+ 2,
235
+ "server should remain healthy after rejecting the malformed session"
236
+ );
237
+ verify.close();
238
+}
239
+
240
+fn test_cgroups_snapshot_handler() -> SnapshotHandler {
241
+ Arc::new(|req, builder| {
242
+ if req.layout_version != 1 || req.flags != 0 {
243
+ return false;
244
+ }
245
+ builder.set_header(1, 42);
246
+ fill_test_cgroups_snapshot(builder)
247
+ })
248
+}
249
+
250
+fn test_cgroups_dispatch() -> DispatchHandler {
251
+ snapshot_dispatch(test_cgroups_snapshot_handler(), 3)
252
+}
253
+
254
+fn increment_handler() -> IncrementHandler {
255
+ Arc::new(|value| Some(value + 1))
256
+}
257
+
258
+fn increment_dispatch_handler() -> DispatchHandler {
259
+ increment_dispatch(increment_handler())
260
+}
261
+
262
+fn string_reverse_handler() -> StringReverseHandler {
263
+ Arc::new(|s| Some(s.chars().rev().collect()))
264
+}
265
+
266
+fn string_reverse_dispatch_handler() -> DispatchHandler {
267
+ string_reverse_dispatch(string_reverse_handler())
268
+}
269
+
270
+struct TestServer {
271
+ stop_flag: Arc<AtomicBool>,
272
+ thread: Option<thread::JoinHandle<()>>,
273
+}
274
+
275
+impl TestServer {
276
+ fn start(service: &str, expected_method_code: u16, handler: Option<DispatchHandler>) -> Self {
277
+ Self::start_with(service, server_config(), expected_method_code, handler, 8)
278
+ }
279
+
280
+ #[cfg(target_os = "linux")]
281
+ fn start_shm(
282
+ service: &str,
283
+ expected_method_code: u16,
284
+ handler: Option<DispatchHandler>,
285
+ ) -> Self {
286
+ Self::start_with(
287
+ service,
288
+ shm_server_config(),
289
+ expected_method_code,
290
+ handler,
291
+ 8,
292
+ )
293
+ }
294
+
295
+ fn start_with_workers(
296
+ service: &str,
297
+ expected_method_code: u16,
298
+ handler: Option<DispatchHandler>,
299
+ worker_count: usize,
300
+ ) -> Self {
301
+ Self::start_with(
302
+ service,
303
+ server_config(),
304
+ expected_method_code,
305
+ handler,
306
+ worker_count,
307
+ )
308
+ }
309
+
310
+ fn start_with(
311
+ service: &str,
312
+ config: ServerConfig,
313
+ expected_method_code: u16,
314
+ handler: Option<DispatchHandler>,
315
+ worker_count: usize,
316
+ ) -> Self {
317
+ ensure_run_dir();
318
+ cleanup_all(service);
319
+
320
+ let svc = service.to_string();
321
+ let ready_flag = Arc::new(AtomicBool::new(false));
322
+ let ready_clone = ready_flag.clone();
323
+
324
+ let mut server = ManagedServer::with_workers(
325
+ TEST_RUN_DIR,
326
+ &svc,
327
+ config,
328
+ expected_method_code,
329
+ handler,
330
+ worker_count,
331
+ );
332
+ let stop_flag = server.running_flag();
333
+
334
+ let thread = thread::spawn(move || {
335
+ // We need to signal readiness after bind but before accept loop.
336
+ // The run() method binds internally, so we signal immediately
337
+ // after it starts (it blocks on accept).
338
+ ready_clone.store(true, Ordering::Release);
339
+ let _ = server.run();
340
+ });
341
+
342
+ // Wait for server to be ready
343
+ for _ in 0..2000 {
344
+ if ready_flag.load(Ordering::Acquire) {
345
+ break;
346
+ }
347
+ thread::sleep(Duration::from_micros(500));
348
+ }
349
+ wait_for_listener_bind(service);
350
+
351
+ TestServer {
352
+ stop_flag,
353
+ thread: Some(thread),
354
+ }
355
+ }
356
+
357
+ fn start_with_resp_size(
358
+ service: &str,
359
+ expected_method_code: u16,
360
+ handler: Option<DispatchHandler>,
361
+ resp_buf_size: usize,
362
+ ) -> Self {
363
+ ensure_run_dir();
364
+ cleanup_all(service);
365
+
366
+ let svc = service.to_string();
367
+ let ready_flag = Arc::new(AtomicBool::new(false));
368
+ let ready_clone = ready_flag.clone();
369
+
370
+ let mut scfg = server_config();
371
+ scfg.max_response_payload_bytes = resp_buf_size as u32;
372
+
373
+ let mut server =
374
+ ManagedServer::new(TEST_RUN_DIR, &svc, scfg, expected_method_code, handler);
375
+ let stop_flag = server.running_flag();
376
+
377
+ let thread = thread::spawn(move || {
378
+ ready_clone.store(true, Ordering::Release);
379
+ let _ = server.run();
380
+ });
381
+
382
+ for _ in 0..2000 {
383
+ if ready_flag.load(Ordering::Acquire) {
384
+ break;
385
+ }
386
+ thread::sleep(Duration::from_micros(500));
387
+ }
388
+ wait_for_listener_bind(service);
389
+
390
+ TestServer {
391
+ stop_flag,
392
+ thread: Some(thread),
393
+ }
394
+ }
395
+
396
+ fn stop(&mut self) {
397
+ self.stop_flag.store(false, Ordering::Release);
398
+ if let Some(t) = self.thread.take() {
399
+ let _ = t.join();
400
+ }
401
+ }
402
+}
403
+
404
+impl Drop for TestServer {
405
+ fn drop(&mut self) {
406
+ self.stop();
407
+ }
408
+}
409
+
410
+struct RawSessionServer {
411
+ thread: Option<thread::JoinHandle<Result<(), String>>>,
412
+}
413
+
414
+struct RawHelloAckServer {
415
+ thread: Option<thread::JoinHandle<Result<(), String>>>,
416
+}
417
+
418
+fn raw_listener_fd_for_service(service: &str) -> RawFd {
419
+ let path = socket_path(service);
420
+ let path_bytes = path.as_os_str().as_bytes();
421
+ assert!(
422
+ path_bytes.len() < std::mem::size_of::<libc::sockaddr_un>() - 2,
423
+ "socket path too long"
424
+ );
425
+
426
+ let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_SEQPACKET, 0) };
427
+ assert!(
428
+ fd >= 0,
429
+ "socket failed: {}",
430
+ std::io::Error::last_os_error()
431
+ );
432
+
433
+ let mut addr: libc::sockaddr_un = unsafe { std::mem::zeroed() };
434
+ addr.sun_family = libc::AF_UNIX as libc::sa_family_t;
435
+ for (idx, byte) in path_bytes.iter().enumerate() {
436
+ addr.sun_path[idx] = *byte as libc::c_char;
437
+ }
438
+
439
+ let rc = unsafe {
440
+ libc::bind(
441
+ fd,
442
+ &addr as *const libc::sockaddr_un as *const libc::sockaddr,
443
+ std::mem::size_of::<libc::sockaddr_un>() as libc::socklen_t,
444
+ )
445
+ };
446
+ assert_eq!(rc, 0, "bind failed: {}", std::io::Error::last_os_error());
447
+
448
+ let rc = unsafe { libc::listen(fd, 4) };
449
+ assert_eq!(rc, 0, "listen failed: {}", std::io::Error::last_os_error());
450
+ fd
451
+}
452
+
453
+fn hello_ack_packet_with_version(version: u16, status: u16, layout_version: u16) -> Vec<u8> {
454
+ let ack = crate::protocol::HelloAck {
455
+ layout_version,
456
+ flags: 0,
457
+ server_supported_profiles: crate::protocol::PROFILE_BASELINE,
458
+ intersection_profiles: crate::protocol::PROFILE_BASELINE,
459
+ selected_profile: crate::protocol::PROFILE_BASELINE,
460
+ agreed_max_request_payload_bytes: crate::protocol::MAX_PAYLOAD_DEFAULT,
461
+ agreed_max_request_batch_items: 1,
462
+ agreed_max_response_payload_bytes: RESPONSE_BUF_SIZE as u32,
463
+ agreed_max_response_batch_items: 1,
464
+ agreed_packet_size: 0,
465
+ session_id: 77,
466
+ };
467
+
468
+ let mut payload = vec![0u8; 48];
469
+ let payload_len = ack.encode(&mut payload);
470
+ payload.truncate(payload_len);
471
+
472
+ let hdr = crate::protocol::Header {
473
+ magic: crate::protocol::MAGIC_MSG,
474
+ version,
475
+ header_len: crate::protocol::HEADER_LEN,
476
+ kind: crate::protocol::KIND_CONTROL,
477
+ flags: 0,
478
+ code: crate::protocol::CODE_HELLO_ACK,
479
+ transport_status: status,
480
+ payload_len: payload.len() as u32,
481
+ item_count: 1,
482
+ message_id: 0,
483
+ };
484
+
485
+ let mut pkt = vec![0u8; crate::protocol::HEADER_SIZE + payload.len()];
486
+ hdr.encode(&mut pkt[..crate::protocol::HEADER_SIZE]);
487
+ pkt[crate::protocol::HEADER_SIZE..].copy_from_slice(&payload);
488
+ pkt
489
+}
490
+
491
+fn start_raw_hello_ack_server(service: &str, packet: Vec<u8>) -> RawHelloAckServer {
492
+ ensure_run_dir();
493
+ cleanup_all(service);
494
+
495
+ let svc = service.to_string();
496
+ let thread = thread::spawn(move || {
497
+ let fd = raw_listener_fd_for_service(&svc);
498
+ let client_fd = unsafe { libc::accept(fd, std::ptr::null_mut(), std::ptr::null_mut()) };
499
+ if client_fd < 0 {
500
+ unsafe { libc::close(fd) };
501
+ return Err(format!("accept: {}", std::io::Error::last_os_error()));
502
+ }
503
+
504
+ let mut hello_buf = [0u8; crate::protocol::HEADER_SIZE + 128];
505
+ let n = unsafe {
506
+ libc::recv(
507
+ client_fd,
508
+ hello_buf.as_mut_ptr() as *mut libc::c_void,
509
+ hello_buf.len(),
510
+ 0,
511
+ )
512
+ };
513
+ if n < 0 {
514
+ unsafe {
515
+ libc::close(client_fd);
516
+ libc::close(fd);
517
+ }
518
+ return Err(format!("recv: {}", std::io::Error::last_os_error()));
519
+ }
520
+
521
+ let wrote = unsafe {
522
+ libc::send(
523
+ client_fd,
524
+ packet.as_ptr() as *const libc::c_void,
525
+ packet.len(),
526
+ 0,
527
+ )
528
+ };
529
+ unsafe {
530
+ libc::close(client_fd);
531
+ libc::close(fd);
532
+ }
533
+ if wrote != packet.len() as isize {
534
+ return Err(format!(
535
+ "send short write: wrote {wrote}, want {}",
536
+ packet.len()
537
+ ));
538
+ }
539
+
540
+ Ok(())
541
+ });
542
+
543
+ thread::sleep(Duration::from_millis(50));
544
+ RawHelloAckServer {
545
+ thread: Some(thread),
546
+ }
547
+}
548
+
549
+impl RawHelloAckServer {
550
+ fn wait(&mut self) {
551
+ if let Some(thread) = self.thread.take() {
552
+ match thread.join() {
553
+ Ok(Ok(())) => {}
554
+ Ok(Err(err)) => panic!("raw hello-ack server failed: {err}"),
555
+ Err(_) => panic!("raw hello-ack server panicked"),
556
+ }
557
+ }
558
+ }
559
+}
560
+
561
+fn start_raw_session_server<F>(service: &str, cfg: ServerConfig, handler: F) -> RawSessionServer
562
+where
563
+ F: FnOnce(&mut UdsSession, Header, &[u8]) -> Result<(), String> + Send + 'static,
564
+{
565
+ ensure_run_dir();
566
+ cleanup_all(service);
567
+
568
+ let svc = service.to_string();
569
+ let ready = Arc::new(AtomicBool::new(false));
570
+ let ready_clone = ready.clone();
571
+
572
+ let thread = thread::spawn(move || {
573
+ let listener =
574
+ UdsListener::bind(TEST_RUN_DIR, &svc, cfg).map_err(|e| format!("bind: {e}"))?;
575
+ ready_clone.store(true, Ordering::Release);
576
+ let mut session = listener.accept().map_err(|e| format!("accept: {e}"))?;
577
+
578
+ let (hdr, payload) = {
579
+ let mut recv_buf = vec![0u8; RESPONSE_BUF_SIZE];
580
+ let (hdr, payload) = session
581
+ .receive(&mut recv_buf)
582
+ .map_err(|e| format!("receive: {e}"))?;
583
+ (hdr, payload.to_vec())
584
+ };
585
+
586
+ handler(&mut session, hdr, &payload)
587
+ });
588
+
589
+ for _ in 0..2000 {
590
+ if ready.load(Ordering::Acquire) {
591
+ break;
592
+ }
593
+ thread::sleep(Duration::from_micros(500));
594
+ }
595
+ thread::sleep(Duration::from_millis(50));
596
+
597
+ RawSessionServer {
598
+ thread: Some(thread),
599
+ }
600
+}
601
+
602
+impl RawSessionServer {
603
+ fn wait(&mut self) {
604
+ if let Some(thread) = self.thread.take() {
605
+ match thread.join() {
606
+ Ok(Ok(())) => {}
607
+ Ok(Err(err)) => panic!("raw unix session server failed: {err}"),
608
+ Err(_) => panic!("raw unix session server panicked"),
609
+ }
610
+ }
611
+ }
612
+}
613
+
614
+#[cfg(target_os = "linux")]
615
+struct RawShmSessionServer {
616
+ thread: Option<thread::JoinHandle<Result<(), String>>>,
617
+}
618
+
619
+#[cfg(target_os = "linux")]
620
+fn start_raw_shm_session_server<F>(
621
+ service: &str,
622
+ cfg: ServerConfig,
623
+ handler: F,
624
+) -> RawShmSessionServer
625
+where
626
+ F: FnOnce(&mut ShmContext, Header, &[u8]) -> Result<(), String> + Send + 'static,
627
+{
628
+ ensure_run_dir();
629
+ cleanup_all(service);
630
+
631
+ let svc = service.to_string();
632
+ let ready = Arc::new(AtomicBool::new(false));
633
+ let ready_clone = ready.clone();
634
+
635
+ let thread = thread::spawn(move || {
636
+ let listener =
637
+ UdsListener::bind(TEST_RUN_DIR, &svc, cfg).map_err(|e| format!("bind: {e}"))?;
638
+ ready_clone.store(true, Ordering::Release);
639
+ let session = listener.accept().map_err(|e| format!("accept: {e}"))?;
640
+ if session.selected_profile != PROFILE_SHM_FUTEX {
641
+ return Err(format!("unexpected profile {}", session.selected_profile));
642
+ }
643
+
644
+ let mut shm = ShmContext::server_create(
645
+ TEST_RUN_DIR,
646
+ &svc,
647
+ session.session_id,
648
+ session.max_request_payload_bytes + HEADER_SIZE as u32,
649
+ session.max_response_payload_bytes + HEADER_SIZE as u32,
650
+ )
651
+ .map_err(|e| format!("server_create: {e}"))?;
652
+
653
+ let mut recv_buf = vec![0u8; session.max_request_payload_bytes as usize + HEADER_SIZE];
654
+ let mlen = shm
655
+ .receive(&mut recv_buf, 5000)
656
+ .map_err(|e| format!("shm receive: {e}"))?;
657
+ if mlen < HEADER_SIZE {
658
+ return Err(format!("request too short: {mlen}"));
659
+ }
660
+
661
+ let hdr = Header::decode(&recv_buf[..mlen]).map_err(|e| format!("decode: {e:?}"))?;
662
+ let payload = recv_buf[HEADER_SIZE..mlen].to_vec();
663
+ handler(&mut shm, hdr, &payload)
664
+ });
665
+
666
+ for _ in 0..2000 {
667
+ if ready.load(Ordering::Acquire) {
668
+ break;
669
+ }
670
+ thread::sleep(Duration::from_micros(500));
671
+ }
672
+ thread::sleep(Duration::from_millis(50));
673
+
674
+ RawShmSessionServer {
675
+ thread: Some(thread),
676
+ }
677
+}
678
+
679
+#[cfg(target_os = "linux")]
680
+impl RawShmSessionServer {
681
+ fn wait(&mut self) {
682
+ if let Some(thread) = self.thread.take() {
683
+ match thread.join() {
684
+ Ok(Ok(())) => {}
685
+ Ok(Err(err)) => panic!("raw shm session server failed: {err}"),
686
+ Err(_) => panic!("raw shm session server panicked"),
687
+ }
688
+ }
689
+ }
690
+}
691
+
692
+#[cfg(target_os = "linux")]
693
+fn encode_raw_message(hdr: &Header, payload: &[u8]) -> Vec<u8> {
694
+ let mut msg = vec![0u8; HEADER_SIZE + payload.len()];
695
+ hdr.encode(&mut msg[..HEADER_SIZE]);
696
+ if !payload.is_empty() {
697
+ msg[HEADER_SIZE..].copy_from_slice(payload);
698
+ }
699
+ msg
700
+}
701
+
702
+#[test]
703
+fn test_client_lifecycle() {
704
+ let svc = "rs_svc_lifecycle";
705
+ ensure_run_dir();
706
+ cleanup_all(svc);
707
+
708
+ // Init without server running
709
+ let mut client = snapshot_client(svc, client_config());
710
+ assert_eq!(client.state, ClientState::Disconnected);
711
+ assert!(!client.ready());
712
+
713
+ // Refresh without server -> NOT_FOUND
714
+ let changed = client.refresh();
715
+ assert!(changed);
716
+ assert_eq!(client.state, ClientState::NotFound);
717
+
718
+ // Start server
719
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
720
+
721
+ // Refresh -> READY
722
+ let changed = client.refresh();
723
+ assert!(changed);
724
+ assert_eq!(client.state, ClientState::Ready);
725
+ assert!(client.ready());
726
+
727
+ // Status reporting
728
+ let status = client.status();
729
+ assert_eq!(status.connect_count, 1);
730
+ assert_eq!(status.reconnect_count, 0);
731
+
732
+ // Close
733
+ client.close();
734
+ assert_eq!(client.state, ClientState::Disconnected);
735
+ assert!(!client.ready());
736
+
737
+ server.stop();
738
+ cleanup_all(svc);
739
+}
740
+
741
+#[test]
742
+fn test_fill_test_cgroups_snapshot_small_builder_returns_false() {
743
+ let mut buf = [0u8; 64];
744
+ let mut builder = CgroupsBuilder::new(&mut buf, 3, 0, 0);
745
+ assert!(
746
+ !fill_test_cgroups_snapshot(&mut builder),
747
+ "small builder should reject the synthetic snapshot"
748
+ );
749
+}
750
+
751
+#[test]
752
+fn test_snapshot_handler_rejects_bad_request_metadata() {
753
+ let on_snapshot = test_cgroups_snapshot_handler();
754
+ let req = CgroupsRequest {
755
+ layout_version: 2,
756
+ flags: 0,
757
+ };
758
+ let mut response_payload = [0u8; RESPONSE_BUF_SIZE];
759
+ let mut builder = CgroupsBuilder::new(&mut response_payload, 4, 1, 99);
760
+ assert!(
761
+ !on_snapshot(&req, &mut builder),
762
+ "snapshot handler should reject bad request metadata"
763
+ );
764
+}
765
+
766
+#[test]
767
+fn test_cgroups_call() {
768
+ let svc = "rs_svc_cgroups";
769
+ ensure_run_dir();
770
+ cleanup_all(svc);
771
+
772
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
773
+
774
+ let mut client = snapshot_client(svc, client_config());
775
+ client.refresh();
776
+ assert!(client.ready());
777
+
778
+ let view = client.call_snapshot().expect("call should succeed");
779
+
780
+ assert_eq!(view.item_count, 3);
781
+ assert_eq!(view.systemd_enabled, 1);
782
+ assert_eq!(view.generation, 42);
783
+
784
+ // Verify first item
785
+ let item0 = view.item(0).expect("item 0");
786
+ assert_eq!(item0.hash, 1001);
787
+ assert_eq!(item0.enabled, 1);
788
+ assert_eq!(item0.name.as_bytes(), b"docker-abc123");
789
+ assert_eq!(item0.path.as_bytes(), b"/sys/fs/cgroup/docker/abc123");
790
+
791
+ // Verify third item
792
+ let item2 = view.item(2).expect("item 2");
793
+ assert_eq!(item2.hash, 3003);
794
+ assert_eq!(item2.enabled, 0);
795
+ assert_eq!(item2.name.as_bytes(), b"systemd-user");
796
+
797
+ // Verify stats
798
+ let status = client.status();
799
+ assert_eq!(status.call_count, 1);
800
+ assert_eq!(status.error_count, 0);
801
+
802
+ client.close();
803
+ server.stop();
804
+ cleanup_all(svc);
805
+}
806
+
807
+#[cfg(target_os = "linux")]
808
+#[test]
809
+fn test_cgroups_call_shm() {
810
+ let svc = "rs_svc_cgroups_shm";
811
+ ensure_run_dir();
812
+ cleanup_all(svc);
813
+
814
+ let mut server =
815
+ TestServer::start_shm(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
816
+
817
+ let mut client = snapshot_client(svc, shm_client_config());
818
+ connect_ready(&mut client);
819
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
820
+ assert_eq!(
821
+ client.session.as_ref().map(|s| s.selected_profile),
822
+ Some(PROFILE_SHM_FUTEX)
823
+ );
824
+
825
+ let view = client.call_snapshot().expect("snapshot over SHM");
826
+ assert_eq!(view.item_count, 3);
827
+ assert_eq!(view.generation, 42);
828
+ assert_eq!(view.item(0).expect("item 0").hash, 1001);
829
+
830
+ client.close();
831
+ server.stop();
832
+ cleanup_all(svc);
833
+}
834
+
835
+#[cfg(target_os = "linux")]
836
+#[test]
837
+fn test_client_call_string_reverse_shm_success() {
838
+ let svc = "rs_svc_strrev_shm";
839
+ ensure_run_dir();
840
+ cleanup_all(svc);
841
+
842
+ let mut server = TestServer::start_shm(
843
+ svc,
844
+ METHOD_STRING_REVERSE,
845
+ Some(string_reverse_dispatch_handler()),
846
+ );
847
+
848
+ let mut client = string_reverse_client(svc, shm_client_config());
849
+ connect_ready(&mut client);
850
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
851
+
852
+ let result = client
853
+ .call_string_reverse("hello")
854
+ .expect("string reverse over SHM");
855
+ assert_eq!(result.as_str(), "olleh");
856
+
857
+ client.close();
858
+ server.stop();
859
+ cleanup_all(svc);
860
+}
861
+
862
+#[cfg(target_os = "linux")]
863
+#[test]
864
+fn test_increment_batch_shm() {
865
+ let svc = "rs_pp_batch_shm";
866
+ ensure_run_dir();
867
+ cleanup_all(svc);
868
+
869
+ let mut server =
870
+ TestServer::start_shm(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
871
+
872
+ let mut client = increment_client(svc, shm_client_config());
873
+ connect_ready(&mut client);
874
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
875
+
876
+ let values = vec![10u64, 20, 30, 40];
877
+ let results = client
878
+ .call_increment_batch(&values)
879
+ .expect("batch over SHM");
880
+ assert_eq!(results, vec![11, 21, 31, 41]);
881
+
882
+ client.close();
883
+ server.stop();
884
+ cleanup_all(svc);
885
+}
886
+
887
+#[cfg(target_os = "linux")]
888
+#[test]
889
+fn test_refresh_shm_attach_failure_falls_back_to_baseline() {
890
+ let svc = "rs_svc_shm_attach_fail";
891
+ ensure_run_dir();
892
+ cleanup_all(svc);
893
+
894
+ let ready = Arc::new(AtomicBool::new(false));
895
+ let ready_clone = ready.clone();
896
+ let svc_clone = svc.to_string();
897
+
898
+ let server_thread = thread::spawn(move || {
899
+ let listener = UdsListener::bind(TEST_RUN_DIR, &svc_clone, shm_server_config())
900
+ .expect("bind shm listener");
901
+ ready_clone.store(true, Ordering::Release);
902
+ let mut first = listener.accept().expect("accept negotiated shm session");
903
+ assert_eq!(first.selected_profile, PROFILE_SHM_FUTEX);
904
+
905
+ let mut recv_buf = vec![0u8; RESPONSE_BUF_SIZE];
906
+ let first_result = first.receive(&mut recv_buf);
907
+ assert!(
908
+ first_result.is_err(),
909
+ "first SHM-selected session should disconnect after attach failure"
910
+ );
911
+
912
+ let mut second = listener.accept().expect("accept baseline fallback session");
913
+ assert_eq!(second.selected_profile, PROFILE_BASELINE);
914
+
915
+ let second_result = second.receive(&mut recv_buf);
916
+ assert!(
917
+ second_result.is_err(),
918
+ "second baseline session should close cleanly when client closes"
919
+ );
920
+ });
921
+
922
+ while !ready.load(Ordering::Acquire) {
923
+ thread::sleep(Duration::from_millis(1));
924
+ }
925
+
926
+ let mut client = snapshot_client(svc, shm_client_config());
927
+ assert!(
928
+ client.refresh(),
929
+ "refresh should transition to READY via baseline fallback"
930
+ );
931
+ assert_eq!(client.state, ClientState::Ready);
932
+ assert!(client.ready());
933
+ assert!(
934
+ client.session.is_some(),
935
+ "attach fallback should end with a live baseline session"
936
+ );
937
+ assert!(
938
+ client.shm.is_none(),
939
+ "fallback session must not retain SHM state"
940
+ );
941
+ assert_eq!(
942
+ client.session.as_ref().map(|s| s.selected_profile),
943
+ Some(PROFILE_BASELINE)
944
+ );
945
+ assert_eq!(client.transport_config.supported_profiles, PROFILE_BASELINE);
946
+ assert_eq!(client.transport_config.preferred_profiles, 0);
947
+
948
+ client.close();
949
+ server_thread.join().expect("server join");
950
+ cleanup_all(svc);
951
+}
952
+
953
+#[cfg(target_os = "linux")]
954
+#[test]
955
+fn test_server_falls_back_to_baseline_when_linux_shm_prepare_fails() {
956
+ let svc = "rs_svc_shm_upgrade_fail";
957
+ ensure_run_dir();
958
+ cleanup_all(svc);
959
+
960
+ let shm_path = format!("{TEST_RUN_DIR}/{svc}-{:016x}.ipcshm", 1u64);
961
+ let _ = std::fs::remove_dir_all(&shm_path);
962
+ std::fs::create_dir(&shm_path).expect("create SHM obstruction directory");
963
+
964
+ let mut server = TestServer::start_with(
965
+ svc,
966
+ shm_server_config(),
967
+ METHOD_INCREMENT,
968
+ Some(increment_dispatch_handler()),
969
+ 8,
970
+ );
971
+ let mut client = increment_client(svc, shm_client_config());
972
+
973
+ assert!(client.refresh(), "client should transition to READY");
974
+ assert!(client.ready(), "client should remain usable over baseline");
975
+ assert_eq!(client.state, ClientState::Ready);
976
+ assert_eq!(
977
+ client.session.as_ref().map(|s| s.selected_profile),
978
+ Some(PROFILE_BASELINE)
979
+ );
980
+ assert!(client.shm.is_none(), "fallback session must not attach SHM");
981
+
982
+ client.close();
983
+ server.stop();
984
+ let _ = std::fs::remove_dir_all(&shm_path);
985
+ cleanup_all(svc);
986
+}
987
+
988
+#[cfg(target_os = "linux")]
989
+#[test]
990
+fn test_call_increment_shm_short_response_truncated() {
991
+ let svc = "rs_svc_shm_short_resp";
992
+ ensure_run_dir();
993
+ cleanup_all(svc);
994
+
995
+ let ready = Arc::new(AtomicBool::new(false));
996
+ let ready_clone = ready.clone();
997
+ let svc_clone = svc.to_string();
998
+
999
+ let server_thread = thread::spawn(move || -> Result<(), String> {
1000
+ let listener = UdsListener::bind(TEST_RUN_DIR, &svc_clone, shm_server_config())
1001
+ .map_err(|e| format!("bind: {e}"))?;
1002
+ ready_clone.store(true, Ordering::Release);
1003
+ let session = listener.accept().map_err(|e| format!("accept: {e}"))?;
1004
+ if session.selected_profile != PROFILE_SHM_FUTEX {
1005
+ return Err(format!("unexpected profile {}", session.selected_profile));
1006
+ }
1007
+
1008
+ let mut shm = ShmContext::server_create(
1009
+ TEST_RUN_DIR,
1010
+ &svc_clone,
1011
+ session.session_id,
1012
+ session.max_request_payload_bytes + HEADER_SIZE as u32,
1013
+ session.max_response_payload_bytes + HEADER_SIZE as u32,
1014
+ )
1015
+ .map_err(|e| format!("server_create: {e}"))?;
1016
+
1017
+ let mut req_buf = vec![0u8; session.max_request_payload_bytes as usize + HEADER_SIZE];
1018
+ let mlen = shm
1019
+ .receive(&mut req_buf, 5000)
1020
+ .map_err(|e| format!("shm receive: {e}"))?;
1021
+ if mlen < HEADER_SIZE {
1022
+ return Err(format!("request too short: {mlen}"));
1023
+ }
1024
+
1025
+ shm.send(&[0xAB])
1026
+ .map_err(|e| format!("shm send short response: {e}"))?;
1027
+ Ok(())
1028
+ });
1029
+
1030
+ while !ready.load(Ordering::Acquire) {
1031
+ thread::sleep(Duration::from_millis(1));
1032
+ }
1033
+
1034
+ let mut client = increment_client(svc, shm_client_config());
1035
+ connect_ready(&mut client);
1036
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
1037
+
1038
+ let err = client
1039
+ .call_increment(41)
1040
+ .expect_err("short SHM response should be truncated");
1041
+ assert_eq!(err, NipcError::Truncated);
1042
+
1043
+ client.close();
1044
+ match server_thread.join() {
1045
+ Ok(Ok(())) => {}
1046
+ Ok(Err(err)) => panic!("raw shm server failed: {err}"),
1047
+ Err(_) => panic!("raw shm server panicked"),
1048
+ }
1049
+ cleanup_all(svc);
1050
+}
1051
+
1052
+#[cfg(target_os = "linux")]
1053
+#[test]
1054
+fn test_raw_shm_session_server_wait_panics_on_baseline_profile() {
1055
+ let svc = "rs_svc_shm_helper_bad_profile";
1056
+ ensure_run_dir();
1057
+ cleanup_all(svc);
1058
+
1059
+ let mut server = start_raw_shm_session_server(svc, server_config(), move |_, _, _| Ok(()));
1060
+
1061
+ let mut client = increment_client(svc, client_config());
1062
+ connect_ready(&mut client);
1063
+ client.close();
1064
+
1065
+ let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| server.wait()))
1066
+ .expect_err("baseline profile should panic in raw SHM helper");
1067
+ let msg = panic_payload_to_string(panic);
1068
+ assert!(
1069
+ msg.contains("unexpected profile"),
1070
+ "unexpected panic message: {msg}"
1071
+ );
1072
+
1073
+ cleanup_all(svc);
1074
+}
1075
+
1076
+#[cfg(target_os = "linux")]
1077
+#[test]
1078
+fn test_raw_shm_session_server_wait_panics_on_short_request() {
1079
+ let svc = "rs_svc_shm_helper_short_req";
1080
+ ensure_run_dir();
1081
+ cleanup_all(svc);
1082
+
1083
+ let mut server = start_raw_shm_session_server(svc, shm_server_config(), move |_, _, _| Ok(()));
1084
+
1085
+ let mut client = increment_client(svc, shm_client_config());
1086
+ connect_ready(&mut client);
1087
+ assert!(
1088
+ client.shm.is_some(),
1089
+ "expected SHM transport to be negotiated"
1090
+ );
1091
+
1092
+ client
1093
+ .shm
1094
+ .as_mut()
1095
+ .expect("shm")
1096
+ .send(&[0xAB])
1097
+ .expect("send short SHM request");
1098
+ client.close();
1099
+
1100
+ let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| server.wait()))
1101
+ .expect_err("short SHM request should panic in raw SHM helper");
1102
+ let msg = panic_payload_to_string(panic);
1103
+ assert!(
1104
+ msg.contains("request too short"),
1105
+ "unexpected panic message: {msg}"
1106
+ );
1107
+
1108
+ cleanup_all(svc);
1109
+}
1110
+
1111
+#[cfg(target_os = "linux")]
1112
+#[test]
1113
+fn test_call_increment_shm_server_thread_rejects_baseline_profile() {
1114
+ let svc = "rs_svc_shm_short_resp_bad_profile";
1115
+ ensure_run_dir();
1116
+ cleanup_all(svc);
1117
+
1118
+ let ready = Arc::new(AtomicBool::new(false));
1119
+ let ready_clone = ready.clone();
1120
+ let svc_clone = svc.to_string();
1121
+
1122
+ let server_thread = thread::spawn(move || -> Result<(), String> {
1123
+ let listener = UdsListener::bind(TEST_RUN_DIR, &svc_clone, shm_server_config())
1124
+ .map_err(|e| format!("bind: {e}"))?;
1125
+ ready_clone.store(true, Ordering::Release);
1126
+ let session = listener.accept().map_err(|e| format!("accept: {e}"))?;
1127
+ if session.selected_profile != PROFILE_SHM_FUTEX {
1128
+ return Err(format!("unexpected profile {}", session.selected_profile));
1129
+ }
1130
+ Ok(())
1131
+ });
1132
+
1133
+ while !ready.load(Ordering::Acquire) {
1134
+ thread::sleep(Duration::from_millis(1));
1135
+ }
1136
+
1137
+ let mut client = increment_client(svc, client_config());
1138
+ connect_ready(&mut client);
1139
+ client.close();
1140
+
1141
+ match server_thread.join() {
1142
+ Ok(Err(err)) => assert!(
1143
+ err.contains("unexpected profile"),
1144
+ "unexpected error: {err}"
1145
+ ),
1146
+ Ok(Ok(())) => panic!("baseline profile should not satisfy the SHM-only helper"),
1147
+ Err(_) => panic!("raw shm server panicked"),
1148
+ }
1149
+
1150
+ cleanup_all(svc);
1151
+}
1152
+
1153
+#[cfg(target_os = "linux")]
1154
+#[test]
1155
+fn test_call_increment_shm_server_thread_rejects_short_request() {
1156
+ let svc = "rs_svc_shm_short_resp_short_req";
1157
+ ensure_run_dir();
1158
+ cleanup_all(svc);
1159
+
1160
+ let ready = Arc::new(AtomicBool::new(false));
1161
+ let ready_clone = ready.clone();
1162
+ let svc_clone = svc.to_string();
1163
+
1164
+ let server_thread = thread::spawn(move || -> Result<(), String> {
1165
+ let listener = UdsListener::bind(TEST_RUN_DIR, &svc_clone, shm_server_config())
1166
+ .map_err(|e| format!("bind: {e}"))?;
1167
+ ready_clone.store(true, Ordering::Release);
1168
+ let session = listener.accept().map_err(|e| format!("accept: {e}"))?;
1169
+ if session.selected_profile != PROFILE_SHM_FUTEX {
1170
+ return Err(format!("unexpected profile {}", session.selected_profile));
1171
+ }
1172
+
1173
+ let mut shm = ShmContext::server_create(
1174
+ TEST_RUN_DIR,
1175
+ &svc_clone,
1176
+ session.session_id,
1177
+ session.max_request_payload_bytes + HEADER_SIZE as u32,
1178
+ session.max_response_payload_bytes + HEADER_SIZE as u32,
1179
+ )
1180
+ .map_err(|e| format!("server_create: {e}"))?;
1181
+
1182
+ let mut req_buf = vec![0u8; session.max_request_payload_bytes as usize + HEADER_SIZE];
1183
+ let mlen = shm
1184
+ .receive(&mut req_buf, 5000)
1185
+ .map_err(|e| format!("shm receive: {e}"))?;
1186
+ if mlen < HEADER_SIZE {
1187
+ return Err(format!("request too short: {mlen}"));
1188
+ }
1189
+
1190
+ Ok(())
1191
+ });
1192
+
1193
+ while !ready.load(Ordering::Acquire) {
1194
+ thread::sleep(Duration::from_millis(1));
1195
+ }
1196
+
1197
+ let mut client = increment_client(svc, shm_client_config());
1198
+ connect_ready(&mut client);
1199
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
1200
+ client
1201
+ .shm
1202
+ .as_mut()
1203
+ .expect("shm")
1204
+ .send(&[0xAB])
1205
+ .expect("send short SHM request");
1206
+ client.close();
1207
+
1208
+ match server_thread.join() {
1209
+ Ok(Err(err)) => assert!(err.contains("request too short"), "unexpected error: {err}"),
1210
+ Ok(Ok(())) => panic!("short SHM request should not be accepted"),
1211
+ Err(_) => panic!("raw shm server panicked"),
1212
+ }
1213
+
1214
+ cleanup_all(svc);
1215
+}
1216
+
1217
+#[cfg(target_os = "linux")]
1218
+#[test]
1219
+fn test_call_increment_shm_rejects_bad_message_id() {
1220
+ let svc = "rs_svc_shm_inc_bad_mid";
1221
+ ensure_run_dir();
1222
+ cleanup_all(svc);
1223
+
1224
+ let mut server =
1225
+ start_raw_shm_session_server(svc, shm_server_config(), move |shm, req_hdr, _| {
1226
+ let mut payload = [0u8; INCREMENT_PAYLOAD_SIZE];
1227
+ let n = increment_encode(43, &mut payload);
1228
+ if n != INCREMENT_PAYLOAD_SIZE {
1229
+ return Err(format!("increment_encode returned {n}"));
1230
+ }
1231
+
1232
+ let resp_hdr = Header {
1233
+ magic: MAGIC_MSG,
1234
+ version: VERSION,
1235
+ header_len: protocol::HEADER_LEN,
1236
+ kind: KIND_RESPONSE,
1237
+ code: METHOD_INCREMENT,
1238
+ flags: 0,
1239
+ payload_len: n as u32,
1240
+ item_count: 1,
1241
+ message_id: req_hdr.message_id + 1,
1242
+ transport_status: STATUS_OK,
1243
+ };
1244
+ let msg = encode_raw_message(&resp_hdr, &payload[..n]);
1245
+ shm.send(&msg).map_err(|e| format!("shm send: {e}"))
1246
+ });
1247
+
1248
+ let mut client = increment_client(svc, shm_client_config());
1249
+ connect_ready(&mut client);
1250
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
1251
+
1252
+ let err = client
1253
+ .call_increment(42)
1254
+ .expect_err("bad SHM response message_id");
1255
+ assert_eq!(err, NipcError::BadLayout);
1256
+
1257
+ client.close();
1258
+ server.wait();
1259
+ cleanup_all(svc);
1260
+}
1261
+
1262
+#[cfg(target_os = "linux")]
1263
+#[test]
1264
+fn test_call_string_reverse_shm_rejects_bad_message_id() {
1265
+ let svc = "rs_svc_shm_str_bad_mid";
1266
+ ensure_run_dir();
1267
+ cleanup_all(svc);
1268
+
1269
+ let mut server =
1270
+ start_raw_shm_session_server(svc, shm_server_config(), move |shm, req_hdr, _| {
1271
+ let mut payload = [0u8; 128];
1272
+ let n = string_reverse_encode(b"olleh", &mut payload);
1273
+ if n == 0 {
1274
+ return Err("string_reverse_encode returned 0".into());
1275
+ }
1276
+
1277
+ let resp_hdr = Header {
1278
+ magic: MAGIC_MSG,
1279
+ version: VERSION,
1280
+ header_len: protocol::HEADER_LEN,
1281
+ kind: KIND_RESPONSE,
1282
+ code: METHOD_STRING_REVERSE,
1283
+ flags: 0,
1284
+ payload_len: n as u32,
1285
+ item_count: 1,
1286
+ message_id: req_hdr.message_id + 1,
1287
+ transport_status: STATUS_OK,
1288
+ };
1289
+ let msg = encode_raw_message(&resp_hdr, &payload[..n]);
1290
+ shm.send(&msg).map_err(|e| format!("shm send: {e}"))
1291
+ });
1292
+
1293
+ let mut client = string_reverse_client(svc, shm_client_config());
1294
+ connect_ready(&mut client);
1295
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
1296
+
1297
+ let err = client
1298
+ .call_string_reverse("hello")
1299
+ .expect_err("bad SHM response message_id");
1300
+ assert_eq!(err, NipcError::BadLayout);
1301
+
1302
+ client.close();
1303
+ server.wait();
1304
+ cleanup_all(svc);
1305
+}
1306
+
1307
+#[cfg(target_os = "linux")]
1308
+#[test]
1309
+fn test_call_increment_batch_shm_rejects_bad_message_id() {
1310
+ let svc = "rs_svc_shm_batch_bad_mid";
1311
+ ensure_run_dir();
1312
+ cleanup_all(svc);
1313
+
1314
+ let mut server =
1315
+ start_raw_shm_session_server(svc, shm_server_config(), move |shm, req_hdr, _| {
1316
+ let mut encoded = [0u8; INCREMENT_PAYLOAD_SIZE];
1317
+ let n = increment_encode(11, &mut encoded);
1318
+ if n != INCREMENT_PAYLOAD_SIZE {
1319
+ return Err(format!("increment_encode returned {n}"));
1320
+ }
1321
+
1322
+ let mut response_buf = vec![0u8; 128];
1323
+ let resp_len = {
1324
+ let mut batch = BatchBuilder::new(&mut response_buf, 2);
1325
+ batch
1326
+ .add(&encoded)
1327
+ .map_err(|e| format!("batch add 1: {e:?}"))?;
1328
+ batch
1329
+ .add(&encoded)
1330
+ .map_err(|e| format!("batch add 2: {e:?}"))?;
1331
+ let (len, _count) = batch.finish();
1332
+ len
1333
+ };
1334
+
1335
+ let resp_hdr = Header {
1336
+ magic: MAGIC_MSG,
1337
+ version: VERSION,
1338
+ header_len: protocol::HEADER_LEN,
1339
+ kind: KIND_RESPONSE,
1340
+ code: METHOD_INCREMENT,
1341
+ flags: FLAG_BATCH,
1342
+ payload_len: resp_len as u32,
1343
+ item_count: 2,
1344
+ message_id: req_hdr.message_id + 1,
1345
+ transport_status: STATUS_OK,
1346
+ };
1347
+ let msg = encode_raw_message(&resp_hdr, &response_buf[..resp_len]);
1348
+ shm.send(&msg).map_err(|e| format!("shm send: {e}"))
1349
+ });
1350
+
1351
+ let mut client = increment_client(svc, shm_client_config());
1352
+ connect_ready(&mut client);
1353
+ assert!(client.shm.is_some(), "expected SHM to be negotiated");
1354
+
1355
+ let err = client
1356
+ .call_increment_batch(&[10, 20])
1357
+ .expect_err("bad SHM batch response message_id");
1358
+ assert_eq!(err, NipcError::BadLayout);
1359
+
1360
+ client.close();
1361
+ server.wait();
1362
+ cleanup_all(svc);
1363
+}
1364
+
1365
+#[test]
1366
+fn test_retry_on_failure() {
1367
+ let svc = "rs_svc_retry";
1368
+ ensure_run_dir();
1369
+ cleanup_all(svc);
1370
+
1371
+ let mut server1 =
1372
+ TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
1373
+
1374
+ let mut client = snapshot_client(svc, client_config());
1375
+ client.refresh();
1376
+ assert!(client.ready());
1377
+
1378
+ // First call succeeds
1379
+ let view = client.call_snapshot().expect("first call");
1380
+ assert_eq!(view.item_count, 3);
1381
+
1382
+ // Kill server
1383
+ server1.stop();
1384
+ cleanup_all(svc);
1385
+ thread::sleep(Duration::from_millis(50));
1386
+
1387
+ // Restart server
1388
+ let mut server2 =
1389
+ TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
1390
+
1391
+ // Next call triggers reconnect + retry
1392
+ let view2 = client.call_snapshot().expect("retry call");
1393
+ assert_eq!(view2.item_count, 3);
1394
+
1395
+ // Verify reconnect happened
1396
+ let status = client.status();
1397
+ assert!(status.reconnect_count >= 1);
1398
+
1399
+ client.close();
1400
+ server2.stop();
1401
+ cleanup_all(svc);
1402
+}
1403
+
1404
+#[test]
1405
+fn test_string_reverse_retry_on_failure() {
1406
+ let svc = "rs_svc_retry_str";
1407
+ ensure_run_dir();
1408
+ cleanup_all(svc);
1409
+
1410
+ let mut server1 = TestServer::start(
1411
+ svc,
1412
+ METHOD_STRING_REVERSE,
1413
+ Some(string_reverse_dispatch_handler()),
1414
+ );
1415
+
1416
+ let mut client = string_reverse_client(svc, client_config());
1417
+ client.refresh();
1418
+ assert!(client.ready());
1419
+ assert_eq!(
1420
+ client
1421
+ .call_string_reverse("hello")
1422
+ .expect("first reverse")
1423
+ .as_str(),
1424
+ "olleh"
1425
+ );
1426
+
1427
+ server1.stop();
1428
+ cleanup_all(svc);
1429
+ thread::sleep(Duration::from_millis(50));
1430
+
1431
+ let mut server2 = TestServer::start(
1432
+ svc,
1433
+ METHOD_STRING_REVERSE,
1434
+ Some(string_reverse_dispatch_handler()),
1435
+ );
1436
+
1437
+ let result = client.call_string_reverse("hello").expect("retry reverse");
1438
+ assert_eq!(result.as_str(), "olleh");
1439
+
1440
+ let status = client.status();
1441
+ assert!(status.reconnect_count >= 1);
1442
+
1443
+ client.close();
1444
+ server2.stop();
1445
+ cleanup_all(svc);
1446
+}
1447
+
1448
+#[test]
1449
+fn test_increment_batch_retry_on_failure() {
1450
+ let svc = "rs_svc_retry_batch";
1451
+ ensure_run_dir();
1452
+ cleanup_all(svc);
1453
+
1454
+ let mut server1 = TestServer::start_with(
1455
+ svc,
1456
+ batch_server_config(),
1457
+ METHOD_INCREMENT,
1458
+ Some(increment_dispatch_handler()),
1459
+ 8,
1460
+ );
1461
+
1462
+ let mut client = increment_client(svc, batch_client_config());
1463
+ client.refresh();
1464
+ assert!(client.ready());
1465
+ assert_eq!(
1466
+ client.call_increment_batch(&[10, 20]).expect("first batch"),
1467
+ vec![11, 21]
1468
+ );
1469
+
1470
+ server1.stop();
1471
+ cleanup_all(svc);
1472
+ thread::sleep(Duration::from_millis(50));
1473
+
1474
+ let mut server2 = TestServer::start_with(
1475
+ svc,
1476
+ batch_server_config(),
1477
+ METHOD_INCREMENT,
1478
+ Some(increment_dispatch_handler()),
1479
+ 8,
1480
+ );
1481
+
1482
+ let result = client.call_increment_batch(&[10, 20]).expect("retry batch");
1483
+ assert_eq!(result, vec![11, 21]);
1484
+
1485
+ let status = client.status();
1486
+ assert!(status.reconnect_count >= 1);
1487
+
1488
+ client.close();
1489
+ server2.stop();
1490
+ cleanup_all(svc);
1491
+}
1492
+
1493
+#[test]
1494
+fn test_string_reverse_retry_second_failure() {
1495
+ let svc = "rs_svc_retry_str_second_fail";
1496
+ ensure_run_dir();
1497
+ cleanup_all(svc);
1498
+
1499
+ let mut server1 = TestServer::start_with(
1500
+ svc,
1501
+ batch_server_config(),
1502
+ METHOD_STRING_REVERSE,
1503
+ Some(string_reverse_dispatch_handler()),
1504
+ 8,
1505
+ );
1506
+
1507
+ let mut client = string_reverse_client(svc, client_config());
1508
+ client.refresh();
1509
+ assert!(client.ready());
1510
+ assert_eq!(
1511
+ client
1512
+ .call_string_reverse("hello")
1513
+ .expect("initial reverse")
1514
+ .as_str(),
1515
+ "olleh"
1516
+ );
1517
+
1518
+ server1.stop();
1519
+ cleanup_all(svc);
1520
+ thread::sleep(Duration::from_millis(50));
1521
+
1522
+ let mut server2 =
1523
+ start_raw_session_server(svc, server_config(), move |session, req_hdr, payload| {
1524
+ let decoded =
1525
+ string_reverse_decode(payload).map_err(|e| format!("decode request: {e:?}"))?;
1526
+ if decoded.as_str() != "hello" {
1527
+ return Err("unexpected request payload".into());
1528
+ }
1529
+
1530
+ let mut resp_hdr = Header {
1531
+ kind: KIND_REQUEST,
1532
+ code: METHOD_STRING_REVERSE,
1533
+ flags: 0,
1534
+ item_count: 1,
1535
+ message_id: req_hdr.message_id,
1536
+ transport_status: STATUS_OK,
1537
+ ..Header::default()
1538
+ };
1539
+ session
1540
+ .send(&mut resp_hdr, payload)
1541
+ .map_err(|e| format!("send: {e}"))
1542
+ });
1543
+
1544
+ let err = client
1545
+ .call_string_reverse("hello")
1546
+ .expect_err("retry should fail on malformed second response");
1547
+ assert_eq!(err, NipcError::BadKind);
1548
+
1549
+ let status = client.status();
1550
+ assert_eq!(status.state, ClientState::Broken);
1551
+ assert!(status.reconnect_count >= 1);
1552
+ assert!(status.error_count >= 1);
1553
+
1554
+ client.close();
1555
+ server2.wait();
1556
+ cleanup_all(svc);
1557
+}
1558
+
1559
+#[test]
1560
+fn test_increment_batch_retry_second_failure() {
1561
+ let svc = "rs_svc_retry_batch_second_fail";
1562
+ ensure_run_dir();
1563
+ cleanup_all(svc);
1564
+
1565
+ let mut server1 = TestServer::start_with(
1566
+ svc,
1567
+ batch_server_config(),
1568
+ METHOD_INCREMENT,
1569
+ Some(increment_dispatch_handler()),
1570
+ 8,
1571
+ );
1572
+
1573
+ let mut client = increment_client(svc, batch_client_config());
1574
+ client.refresh();
1575
+ assert!(client.ready());
1576
+ assert_eq!(
1577
+ client
1578
+ .call_increment_batch(&[10, 20])
1579
+ .expect("initial batch"),
1580
+ vec![11, 21]
1581
+ );
1582
+
1583
+ server1.stop();
1584
+ cleanup_all(svc);
1585
+ thread::sleep(Duration::from_millis(50));
1586
+
1587
+ let mut server2 = start_raw_session_server(
1588
+ svc,
1589
+ batch_server_config(),
1590
+ move |session, req_hdr, payload| {
1591
+ let (item0, _) =
1592
+ batch_item_get(payload, 2, 0).map_err(|e| format!("decode batch item 0: {e:?}"))?;
1593
+ let (item1, _) =
1594
+ batch_item_get(payload, 2, 1).map_err(|e| format!("decode batch item 1: {e:?}"))?;
1595
+ let v0 = increment_decode(item0).map_err(|e| format!("decode inc0: {e:?}"))?;
1596
+ let v1 = increment_decode(item1).map_err(|e| format!("decode inc1: {e:?}"))?;
1597
+ if v0 != 10 || v1 != 20 {
1598
+ return Err("unexpected batch payload".into());
1599
+ }
1600
+
1601
+ let mut resp_hdr = Header {
1602
+ kind: KIND_REQUEST,
1603
+ code: METHOD_INCREMENT,
1604
+ flags: 0,
1605
+ item_count: 1,
1606
+ message_id: req_hdr.message_id,
1607
+ transport_status: STATUS_OK,
1608
+ ..Header::default()
1609
+ };
1610
+ session
1611
+ .send(&mut resp_hdr, &[])
1612
+ .map_err(|e| format!("send: {e}"))
1613
+ },
1614
+ );
1615
+
1616
+ let err = client
1617
+ .call_increment_batch(&[10, 20])
1618
+ .expect_err("retry should fail on malformed second batch response");
1619
+ assert_eq!(err, NipcError::BadKind);
1620
+
1621
+ let status = client.status();
1622
+ assert_eq!(status.state, ClientState::Broken);
1623
+ assert!(status.reconnect_count >= 1);
1624
+ assert!(status.error_count >= 1);
1625
+
1626
+ client.close();
1627
+ server2.wait();
1628
+ cleanup_all(svc);
1629
+}
1630
+
1631
+#[test]
1632
+fn test_multiple_clients() {
1633
+ let svc = "rs_svc_multi";
1634
+ ensure_run_dir();
1635
+ cleanup_all(svc);
1636
+
1637
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
1638
+
1639
+ // Create and connect client 1
1640
+ let mut client1 = snapshot_client(svc, client_config());
1641
+ client1.refresh();
1642
+ assert!(client1.ready());
1643
+
1644
+ let view1 = client1.call_snapshot().expect("client 1 call");
1645
+ assert_eq!(view1.item_count, 3);
1646
+
1647
+ // Now multi-client: keep client 1 open, connect client 2
1648
+ let mut client2 = snapshot_client(svc, client_config());
1649
+ client2.refresh();
1650
+ assert!(client2.ready());
1651
+
1652
+ let view2 = client2.call_snapshot().expect("client 2 call");
1653
+ assert_eq!(view2.item_count, 3);
1654
+
1655
+ client1.close();
1656
+ client2.close();
1657
+ server.stop();
1658
+ cleanup_all(svc);
1659
+}
1660
+
1661
+#[test]
1662
+fn test_server_rejects_session_at_worker_capacity() {
1663
+ let svc = "rs_svc_worker_capacity";
1664
+ ensure_run_dir();
1665
+ cleanup_all(svc);
1666
+
1667
+ let handler_calls = Arc::new(std::sync::atomic::AtomicU32::new(0));
1668
+ let release = Arc::new(AtomicBool::new(false));
1669
+
1670
+ let handler = {
1671
+ let handler_calls = handler_calls.clone();
1672
+ let release = release.clone();
1673
+ increment_dispatch(Arc::new(move |value| {
1674
+ handler_calls.fetch_add(1, Ordering::AcqRel);
1675
+ while !release.load(Ordering::Acquire) {
1676
+ thread::sleep(Duration::from_millis(1));
1677
+ }
1678
+ Some(value + 1)
1679
+ }))
1680
+ };
1681
+
1682
+ let mut server = TestServer::start_with_workers(svc, METHOD_INCREMENT, Some(handler), 1);
1683
+
1684
+ let (call_tx, call_rx) = std::sync::mpsc::channel();
1685
+ let svc_name = svc.to_string();
1686
+ let caller = thread::spawn(move || {
1687
+ let mut client = increment_client(&svc_name, client_config());
1688
+ connect_ready(&mut client);
1689
+ let result = client.call_increment(41);
1690
+ client.close();
1691
+ call_tx.send(result).expect("send first call result");
1692
+ });
1693
+
1694
+ for _ in 0..500 {
1695
+ if handler_calls.load(Ordering::Acquire) >= 1 {
1696
+ break;
1697
+ }
1698
+ thread::sleep(Duration::from_millis(1));
1699
+ }
1700
+ assert_eq!(
1701
+ handler_calls.load(Ordering::Acquire),
1702
+ 1,
1703
+ "first client should occupy the only worker slot"
1704
+ );
1705
+
1706
+ let mut session2 =
1707
+ UdsSession::connect(TEST_RUN_DIR, svc, &client_config()).expect("second connect");
1708
+ let mut req_hdr = Header {
1709
+ kind: KIND_REQUEST,
1710
+ code: METHOD_INCREMENT,
1711
+ item_count: 1,
1712
+ message_id: 1,
1713
+ ..Header::default()
1714
+ };
1715
+ let mut req_payload = [0u8; INCREMENT_PAYLOAD_SIZE];
1716
+ assert_eq!(
1717
+ increment_encode(9, &mut req_payload),
1718
+ INCREMENT_PAYLOAD_SIZE,
1719
+ "IncrementEncode should fill the request buffer"
1720
+ );
1721
+
1722
+ let send_err = session2.send(&mut req_hdr, &req_payload).err();
1723
+ let recv_ok = if send_err.is_none() {
1724
+ let mut recv_buf = [0u8; HEADER_SIZE + 64];
1725
+ session2.receive(&mut recv_buf).is_ok()
1726
+ } else {
1727
+ false
1728
+ };
1729
+ assert!(
1730
+ !(send_err.is_none() && recv_ok),
1731
+ "second session should be rejected while the server is at worker capacity"
1732
+ );
1733
+ assert_eq!(
1734
+ handler_calls.load(Ordering::Acquire),
1735
+ 1,
1736
+ "second session should not enter the handler while the first session is active"
1737
+ );
1738
+ drop(session2);
1739
+
1740
+ release.store(true, Ordering::Release);
1741
+ let first_result = call_rx.recv().expect("first call result");
1742
+ assert_eq!(first_result.expect("first call should succeed"), 42);
1743
+ caller.join().expect("caller join");
1744
+
1745
+ thread::sleep(Duration::from_millis(100));
1746
+
1747
+ let mut verify = increment_client(svc, client_config());
1748
+ connect_ready(&mut verify);
1749
+ assert_eq!(
1750
+ verify.call_increment(1).expect("verification call"),
1751
+ 2,
1752
+ "server should remain healthy after rejecting a session at capacity"
1753
+ );
1754
+ verify.close();
1755
+
1756
+ server.stop();
1757
+ cleanup_all(svc);
1758
+}
1759
+
1760
+#[test]
1761
+fn test_poll_fd_invalid_fd_returns_error() {
1762
+ let mut fds = [0; 2];
1763
+ let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
1764
+ assert_eq!(rc, 0, "pipe failed");
1765
+
1766
+ let read_fd = fds[0];
1767
+ let write_fd = fds[1];
1768
+ unsafe {
1769
+ libc::close(read_fd);
1770
+ }
1771
+
1772
+ let rc = poll_fd(read_fd, 0);
1773
+ unsafe {
1774
+ libc::close(write_fd);
1775
+ }
1776
+
1777
+ assert_eq!(rc, -1);
1778
+}
1779
+
1780
+#[test]
1781
+fn test_poll_fd_readable_returns_ready() {
1782
+ let mut fds = [0; 2];
1783
+ let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
1784
+ assert_eq!(rc, 0, "pipe failed");
1785
+
1786
+ let read_fd = fds[0];
1787
+ let write_fd = fds[1];
1788
+ let wrote = unsafe { libc::write(write_fd, b"x".as_ptr() as *const libc::c_void, 1) };
1789
+ assert_eq!(wrote, 1, "write failed");
1790
+
1791
+ let rc = poll_fd(read_fd, 0);
1792
+
1793
+ unsafe {
1794
+ libc::close(read_fd);
1795
+ libc::close(write_fd);
1796
+ }
1797
+
1798
+ assert_eq!(rc, 1);
1799
+}
1800
+
1801
+#[cfg(target_os = "linux")]
1802
+#[test]
1803
+fn test_poll_fd_eintr_returns_timeout() {
1804
+ unsafe extern "C" fn noop_signal_handler(_: libc::c_int) {}
1805
+
1806
+ let mut fds = [0; 2];
1807
+ let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
1808
+ assert_eq!(rc, 0, "pipe failed");
1809
+
1810
+ let read_fd = fds[0];
1811
+ let write_fd = fds[1];
1812
+ let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
1813
+ let mut old_action: libc::sigaction = unsafe { std::mem::zeroed() };
1814
+ action.sa_flags = 0;
1815
+ action.sa_sigaction = noop_signal_handler as usize;
1816
+ unsafe { libc::sigemptyset(&mut action.sa_mask) };
1817
+ assert_eq!(
1818
+ unsafe { libc::sigaction(libc::SIGUSR1, &action, &mut old_action) },
1819
+ 0
1820
+ );
1821
+
1822
+ let tid = unsafe { libc::pthread_self() };
1823
+ let signaler = thread::spawn(move || {
1824
+ thread::sleep(Duration::from_millis(50));
1825
+ assert_eq!(unsafe { libc::pthread_kill(tid, libc::SIGUSR1) }, 0);
1826
+ });
1827
+
1828
+ let rc = poll_fd(read_fd, 5000);
1829
+ signaler.join().expect("signaler join");
1830
+ unsafe {
1831
+ libc::sigaction(libc::SIGUSR1, &old_action, std::ptr::null_mut());
1832
+ libc::close(read_fd);
1833
+ libc::close(write_fd);
1834
+ }
1835
+
1836
+ assert_eq!(rc, 0);
1837
+}
1838
+
1839
+#[test]
1840
+fn test_managed_server_recovers_after_short_uds_request() {
1841
+ let svc = "rs_svc_short_uds_req";
1842
+ ensure_run_dir();
1843
+ cleanup_all(svc);
1844
+
1845
+ let mut server = TestServer::start(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
1846
+ let session = UdsSession::connect(TEST_RUN_DIR, svc, &client_config()).expect("connect");
1847
+ send_raw_packet(session.fd(), &[0xAB]);
1848
+ drop(session);
1849
+ thread::sleep(Duration::from_millis(50));
1850
+
1851
+ verify_increment_service_ok(svc, client_config());
1852
+
1853
+ server.stop();
1854
+ cleanup_all(svc);
1855
+}
1856
+
1857
+#[test]
1858
+fn test_managed_server_recovers_after_bad_uds_header() {
1859
+ let svc = "rs_svc_bad_uds_hdr";
1860
+ ensure_run_dir();
1861
+ cleanup_all(svc);
1862
+
1863
+ let mut server = TestServer::start(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
1864
+ let session = UdsSession::connect(TEST_RUN_DIR, svc, &client_config()).expect("connect");
1865
+ let bad_header = [0u8; HEADER_SIZE];
1866
+ send_raw_packet(session.fd(), &bad_header);
1867
+ drop(session);
1868
+ thread::sleep(Duration::from_millis(50));
1869
+
1870
+ verify_increment_service_ok(svc, client_config());
1871
+
1872
+ server.stop();
1873
+ cleanup_all(svc);
1874
+}
1875
+
1876
+#[test]
1877
+fn test_managed_server_recovers_after_uds_peer_closes_before_response() {
1878
+ let svc = "rs_svc_uds_send_break";
1879
+ ensure_run_dir();
1880
+ cleanup_all(svc);
1881
+
1882
+ let handler = increment_dispatch(Arc::new(|value| {
1883
+ thread::sleep(Duration::from_millis(50));
1884
+ Some(value + 1)
1885
+ }));
1886
+ let mut server = TestServer::start(svc, METHOD_INCREMENT, Some(handler));
1887
+ let session = UdsSession::connect(TEST_RUN_DIR, svc, &client_config()).expect("connect");
1888
+ let request = build_increment_request_message(77, 41);
1889
+ send_raw_packet(session.fd(), &request);
1890
+ assert_eq!(unsafe { libc::shutdown(session.fd(), libc::SHUT_RDWR) }, 0);
1891
+ drop(session);
1892
+ thread::sleep(Duration::from_millis(100));
1893
+
1894
+ verify_increment_service_ok(svc, client_config());
1895
+
1896
+ server.stop();
1897
+ cleanup_all(svc);
1898
+}
1899
+
1900
+#[cfg(target_os = "linux")]
1901
+#[test]
1902
+fn test_managed_server_recovers_after_short_shm_request() {
1903
+ let svc = "rs_svc_short_shm_req";
1904
+ ensure_run_dir();
1905
+ cleanup_all(svc);
1906
+
1907
+ let mut server =
1908
+ TestServer::start_shm(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
1909
+ let mut client = increment_client(svc, shm_client_config());
1910
+ connect_ready(&mut client);
1911
+ assert!(
1912
+ client.shm.is_some(),
1913
+ "expected SHM transport to be negotiated"
1914
+ );
1915
+
1916
+ client
1917
+ .shm
1918
+ .as_mut()
1919
+ .expect("shm")
1920
+ .send(&[0xAB])
1921
+ .expect("send short SHM request");
1922
+ client.close();
1923
+ thread::sleep(Duration::from_millis(50));
1924
+
1925
+ verify_increment_service_ok(svc, shm_client_config());
1926
+
1927
+ server.stop();
1928
+ cleanup_all(svc);
1929
+}
1930
+
1931
+#[cfg(target_os = "linux")]
1932
+#[test]
1933
+fn test_managed_server_recovers_after_bad_shm_header() {
1934
+ let svc = "rs_svc_bad_shm_hdr";
1935
+ ensure_run_dir();
1936
+ cleanup_all(svc);
1937
+
1938
+ let mut server =
1939
+ TestServer::start_shm(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
1940
+ let mut client = increment_client(svc, shm_client_config());
1941
+ connect_ready(&mut client);
1942
+ assert!(
1943
+ client.shm.is_some(),
1944
+ "expected SHM transport to be negotiated"
1945
+ );
1946
+
1947
+ let bad_header = [0u8; HEADER_SIZE];
1948
+ client
1949
+ .shm
1950
+ .as_mut()
1951
+ .expect("shm")
1952
+ .send(&bad_header)
1953
+ .expect("send bad SHM header");
1954
+ client.close();
1955
+ thread::sleep(Duration::from_millis(50));
1956
+
1957
+ verify_increment_service_ok(svc, shm_client_config());
1958
+
1959
+ server.stop();
1960
+ cleanup_all(svc);
1961
+}
1962
+
1963
+#[test]
1964
+fn test_concurrent_clients() {
1965
+ let svc = "rs_svc_concurrent";
1966
+ ensure_run_dir();
1967
+ cleanup_all(svc);
1968
+
1969
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
1970
+
1971
+ const NUM_CLIENTS: usize = 5;
1972
+ const REQUESTS_PER: usize = 10;
1973
+
1974
+ let mut handles = Vec::new();
1975
+
1976
+ for _ in 0..NUM_CLIENTS {
1977
+ let svc_name = svc.to_string();
1978
+ let handle = thread::spawn(move || {
1979
+ let mut client = snapshot_client(&svc_name, client_config());
1980
+
1981
+ // Connect with retry
1982
+ for _ in 0..100 {
1983
+ client.refresh();
1984
+ if client.ready() {
1985
+ break;
1986
+ }
1987
+ thread::sleep(Duration::from_millis(10));
1988
+ }
1989
+
1990
+ assert!(client.ready(), "client must be ready");
1991
+
1992
+ let mut successes = 0usize;
1993
+ for _ in 0..REQUESTS_PER {
1994
+ match client.call_snapshot() {
1995
+ Ok(view) => {
1996
+ assert_eq!(view.item_count, 3);
1997
+ assert_eq!(view.generation, 42);
1998
+
1999
+ // Verify first item content
2000
+ let item0 = view.item(0).expect("item 0");
2001
+ assert_eq!(item0.hash, 1001);
2002
+ assert_eq!(
2003
+ std::str::from_utf8(item0.name.as_bytes()).unwrap(),
2004
+ "docker-abc123"
2005
+ );
2006
+
2007
+ successes += 1;
2008
+ }
2009
+ Err(e) => panic!("call failed: {:?}", e),
2010
+ }
2011
+ }
2012
+ client.close();
2013
+ successes
2014
+ });
2015
+ handles.push(handle);
2016
+ }
2017
+
2018
+ let mut total = 0usize;
2019
+ for h in handles {
2020
+ total += h.join().expect("client thread panicked");
2021
+ }
2022
+
2023
+ assert_eq!(total, NUM_CLIENTS * REQUESTS_PER);
2024
+
2025
+ server.stop();
2026
+ cleanup_all(svc);
2027
+}
2028
+
2029
+#[test]
2030
+fn test_handler_failure() {
2031
+ let svc = "rs_svc_hfail";
2032
+ ensure_run_dir();
2033
+ cleanup_all(svc);
2034
+
2035
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, None);
2036
+
2037
+ let mut client = snapshot_client(svc, client_config());
2038
+ client.refresh();
2039
+ assert!(client.ready());
2040
+
2041
+ // Call should fail (handler returns None -> INTERNAL_ERROR)
2042
+ let err = client.call_snapshot();
2043
+ assert!(err.is_err());
2044
+
2045
+ let status = client.status();
2046
+ assert!(status.error_count >= 1);
2047
+
2048
+ client.close();
2049
+ server.stop();
2050
+ cleanup_all(svc);
2051
+}
2052
+
2053
+#[test]
2054
+fn test_status_reporting() {
2055
+ let svc = "rs_svc_status";
2056
+ ensure_run_dir();
2057
+ cleanup_all(svc);
2058
+
2059
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2060
+
2061
+ let mut client = snapshot_client(svc, client_config());
2062
+ client.refresh();
2063
+ assert!(client.ready());
2064
+
2065
+ // Initial counters
2066
+ let s0 = client.status();
2067
+ assert_eq!(s0.connect_count, 1);
2068
+ assert_eq!(s0.call_count, 0);
2069
+ assert_eq!(s0.error_count, 0);
2070
+
2071
+ // Make 3 successful calls
2072
+ for _ in 0..3 {
2073
+ client.call_snapshot().expect("call ok");
2074
+ }
2075
+
2076
+ let s1 = client.status();
2077
+ assert_eq!(s1.call_count, 3);
2078
+ assert_eq!(s1.error_count, 0);
2079
+
2080
+ // Call on disconnected client
2081
+ client.close();
2082
+ let err = client.call_snapshot();
2083
+ assert!(err.is_err());
2084
+
2085
+ let s2 = client.status();
2086
+ assert_eq!(s2.error_count, 1);
2087
+
2088
+ server.stop();
2089
+ cleanup_all(svc);
2090
+}
2091
+
2092
+#[test]
2093
+fn test_non_request_terminates_session() {
2094
+ // Send a RESPONSE message to a server; the server must terminate
2095
+ // the session (protocol violation), so subsequent requests fail.
2096
+ let svc = "rs_svc_nonreq";
2097
+ ensure_run_dir();
2098
+ cleanup_all(svc);
2099
+
2100
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2101
+
2102
+ // Connect via raw UDS session
2103
+ let mut session = UdsSession::connect(TEST_RUN_DIR, svc, &client_config()).expect("connect");
2104
+
2105
+ // Send a RESPONSE (not REQUEST) - protocol violation
2106
+ let mut hdr = Header {
2107
+ kind: KIND_RESPONSE,
2108
+ code: METHOD_CGROUPS_SNAPSHOT,
2109
+ flags: 0,
2110
+ item_count: 0,
2111
+ message_id: 1,
2112
+ transport_status: STATUS_OK,
2113
+ ..Header::default()
2114
+ };
2115
+ let send_result = session.send(&mut hdr, &[]);
2116
+ // Send may succeed (the bytes go out)
2117
+ if send_result.is_ok() {
2118
+ // But subsequent communication should fail because the
2119
+ // server terminated the session
2120
+ thread::sleep(Duration::from_millis(100));
2121
+ let mut recv_buf = vec![0u8; 4096];
2122
+ // Try to send a valid request and receive - should fail
2123
+ let mut hdr2 = Header {
2124
+ kind: KIND_REQUEST,
2125
+ code: METHOD_CGROUPS_SNAPSHOT,
2126
+ flags: 0,
2127
+ item_count: 1,
2128
+ message_id: 2,
2129
+ transport_status: STATUS_OK,
2130
+ ..Header::default()
2131
+ };
2132
+ let req = CgroupsRequest {
2133
+ layout_version: 1,
2134
+ flags: 0,
2135
+ };
2136
+ let mut req_buf = [0u8; 4];
2137
+ req.encode(&mut req_buf);
2138
+ let _ = session.send(&mut hdr2, &req_buf);
2139
+ let recv = session.receive(&mut recv_buf);
2140
+ assert!(
2141
+ recv.is_err(),
2142
+ "server should have terminated session after non-request message"
2143
+ );
2144
+ }
2145
+
2146
+ drop(session);
2147
+
2148
+ // Verify server is still alive: connect a new client and do a normal call
2149
+ let mut verify_client = snapshot_client(svc, client_config());
2150
+ verify_client.refresh();
2151
+ assert!(
2152
+ verify_client.ready(),
2153
+ "server should still be alive after bad client"
2154
+ );
2155
+
2156
+ let view = verify_client
2157
+ .call_snapshot()
2158
+ .expect("normal call should succeed after bad client");
2159
+ assert_eq!(
2160
+ view.item_count, 3,
2161
+ "response should be correct after bad client"
2162
+ );
2163
+
2164
+ verify_client.close();
2165
+ server.stop();
2166
+ cleanup_all(svc);
2167
+}
2168
+
2169
+// ---------------------------------------------------------------
2170
+// L3 Cache tests
2171
+// ---------------------------------------------------------------
2172
+
2173
+#[test]
2174
+fn test_cache_full_round_trip() {
2175
+ let svc = "rs_cache_rt";
2176
+ ensure_run_dir();
2177
+ cleanup_all(svc);
2178
+
2179
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2180
+
2181
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2182
+ assert!(!cache.ready());
2183
+
2184
+ // Ensure monotonic epoch advances past 0ms before refresh
2185
+ thread::sleep(Duration::from_millis(2));
2186
+
2187
+ // Refresh populates the cache
2188
+ let updated = cache.refresh();
2189
+ assert!(updated);
2190
+ assert!(cache.ready());
2191
+
2192
+ // Lookup by hash + name
2193
+ let item = cache.lookup(1001, "docker-abc123");
2194
+ assert!(item.is_some());
2195
+ let item = item.unwrap();
2196
+ assert_eq!(item.hash, 1001);
2197
+ assert_eq!(item.options, 0);
2198
+ assert_eq!(item.enabled, 1);
2199
+ assert_eq!(item.name, "docker-abc123");
2200
+ assert_eq!(item.path, "/sys/fs/cgroup/docker/abc123");
2201
+
2202
+ let item2 = cache.lookup(3003, "systemd-user");
2203
+ assert!(item2.is_some());
2204
+ assert_eq!(item2.unwrap().enabled, 0);
2205
+
2206
+ // Status
2207
+ let status = cache.status();
2208
+ assert!(status.populated);
2209
+ assert_eq!(status.item_count, 3);
2210
+ assert_eq!(status.systemd_enabled, 1);
2211
+ assert_eq!(status.generation, 42);
2212
+ assert_eq!(status.refresh_success_count, 1);
2213
+ assert_eq!(status.refresh_failure_count, 0);
2214
+ assert_eq!(status.connection_state, ClientState::Ready);
2215
+ assert!(status.last_refresh_ts > 0);
2216
+
2217
+ cache.close();
2218
+ server.stop();
2219
+ cleanup_all(svc);
2220
+}
2221
+
2222
+#[test]
2223
+fn test_cache_refresh_failure_preserves() {
2224
+ let svc = "rs_cache_preserve";
2225
+ ensure_run_dir();
2226
+ cleanup_all(svc);
2227
+
2228
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2229
+
2230
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2231
+
2232
+ // First refresh populates cache
2233
+ assert!(cache.refresh());
2234
+ assert!(cache.ready());
2235
+ assert!(cache.lookup(1001, "docker-abc123").is_some());
2236
+
2237
+ // Kill server
2238
+ server.stop();
2239
+ cleanup_all(svc);
2240
+ thread::sleep(Duration::from_millis(50));
2241
+
2242
+ // Refresh fails, but old cache is preserved
2243
+ let updated = cache.refresh();
2244
+ assert!(!updated);
2245
+ assert!(cache.ready()); // still has cached data
2246
+ assert!(cache.lookup(1001, "docker-abc123").is_some());
2247
+
2248
+ let status = cache.status();
2249
+ assert_eq!(status.refresh_success_count, 1);
2250
+ assert!(status.refresh_failure_count >= 1);
2251
+
2252
+ cache.close();
2253
+ cleanup_all(svc);
2254
+}
2255
+
2256
+#[test]
2257
+fn test_cache_reconnect_rebuilds() {
2258
+ let svc = "rs_cache_reconn";
2259
+ ensure_run_dir();
2260
+ cleanup_all(svc);
2261
+
2262
+ let mut server1 =
2263
+ TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2264
+
2265
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2266
+ assert!(cache.refresh());
2267
+ assert_eq!(cache.status().item_count, 3);
2268
+
2269
+ // Kill and restart server
2270
+ server1.stop();
2271
+ cleanup_all(svc);
2272
+ thread::sleep(Duration::from_millis(50));
2273
+
2274
+ let mut server2 =
2275
+ TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2276
+
2277
+ // Refresh should reconnect and rebuild cache
2278
+ let updated = cache.refresh();
2279
+ assert!(updated);
2280
+ assert!(cache.ready());
2281
+ assert_eq!(cache.status().item_count, 3);
2282
+ assert_eq!(cache.status().refresh_success_count, 2);
2283
+
2284
+ cache.close();
2285
+ server2.stop();
2286
+ cleanup_all(svc);
2287
+}
2288
+
2289
+#[test]
2290
+fn test_cache_lookup_not_found() {
2291
+ let svc = "rs_cache_notfound";
2292
+ ensure_run_dir();
2293
+ cleanup_all(svc);
2294
+
2295
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2296
+
2297
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2298
+ assert!(cache.refresh());
2299
+
2300
+ // Non-existent hash
2301
+ assert!(cache.lookup(9999, "nonexistent").is_none());
2302
+
2303
+ // Correct hash, wrong name
2304
+ assert!(cache.lookup(1001, "wrong-name").is_none());
2305
+
2306
+ // Correct name, wrong hash
2307
+ assert!(cache.lookup(9999, "docker-abc123").is_none());
2308
+
2309
+ cache.close();
2310
+ server.stop();
2311
+ cleanup_all(svc);
2312
+}
2313
+
2314
+#[test]
2315
+fn test_cache_empty() {
2316
+ let svc = "rs_cache_empty";
2317
+ ensure_run_dir();
2318
+ cleanup_all(svc);
2319
+
2320
+ let cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2321
+
2322
+ // Not ready before any refresh
2323
+ assert!(!cache.ready());
2324
+
2325
+ // Lookup on empty cache returns None
2326
+ assert!(cache.lookup(1001, "docker-abc123").is_none());
2327
+
2328
+ let status = cache.status();
2329
+ assert!(!status.populated);
2330
+ assert_eq!(status.item_count, 0);
2331
+ assert_eq!(status.refresh_success_count, 0);
2332
+ assert_eq!(status.refresh_failure_count, 0);
2333
+
2334
+ cleanup_all(svc);
2335
+}
2336
+
2337
+#[test]
2338
+fn test_cache_large_dataset() {
2339
+ let svc = "rs_cache_large";
2340
+ ensure_run_dir();
2341
+ cleanup_all(svc);
2342
+
2343
+ const N: u32 = 1000;
2344
+
2345
+ // Handler that builds N items
2346
+ fn large_snapshot_dispatch() -> DispatchHandler {
2347
+ snapshot_dispatch(
2348
+ Arc::new(|req, builder| {
2349
+ if req.layout_version != 1 || req.flags != 0 {
2350
+ return false;
2351
+ }
2352
+ builder.set_header(1, 100);
2353
+
2354
+ for i in 0..N {
2355
+ let name = format!("cgroup-{i}");
2356
+ let path = format!("/sys/fs/cgroup/test/{i}");
2357
+ if builder
2358
+ .add(
2359
+ i + 1000,
2360
+ 0,
2361
+ if i % 3 == 0 { 0 } else { 1 },
2362
+ name.as_bytes(),
2363
+ path.as_bytes(),
2364
+ )
2365
+ .is_err()
2366
+ {
2367
+ return false;
2368
+ }
2369
+ }
2370
+
2371
+ true
2372
+ }),
2373
+ N,
2374
+ )
2375
+ }
2376
+
2377
+ // Use a larger response buf size
2378
+ let mut cfg = client_config();
2379
+ cfg.max_response_payload_bytes = 256 * N;
2380
+
2381
+ let mut server = TestServer::start_with_resp_size(
2382
+ svc,
2383
+ METHOD_CGROUPS_SNAPSHOT,
2384
+ Some(large_snapshot_dispatch()),
2385
+ 256 * N as usize,
2386
+ );
2387
+
2388
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, cfg);
2389
+
2390
+ assert!(cache.refresh());
2391
+ assert_eq!(cache.status().item_count, N);
2392
+
2393
+ // Verify all lookups
2394
+ for i in 0..N {
2395
+ let name = format!("cgroup-{i}");
2396
+ let item = cache.lookup(i + 1000, &name);
2397
+ assert!(item.is_some(), "item {i} not found");
2398
+ let item = item.unwrap();
2399
+ assert_eq!(item.hash, i + 1000);
2400
+ let expected_path = format!("/sys/fs/cgroup/test/{i}");
2401
+ assert_eq!(item.path, expected_path);
2402
+ }
2403
+
2404
+ cache.close();
2405
+ server.stop();
2406
+ cleanup_all(svc);
2407
+}
2408
+
2409
+#[test]
2410
+fn test_cache_refresh_lossy_utf8() {
2411
+ let svc = "rs_cache_lossy";
2412
+ ensure_run_dir();
2413
+ cleanup_all(svc);
2414
+
2415
+ let handler = snapshot_dispatch(
2416
+ Arc::new(|_, builder| {
2417
+ builder.set_header(1, 7);
2418
+ builder
2419
+ .add(1001, 0, 1, b"bad-\xFF-name", b"/bad/\xFF/path")
2420
+ .is_ok()
2421
+ }),
2422
+ 1,
2423
+ );
2424
+
2425
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(handler));
2426
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2427
+
2428
+ assert!(cache.refresh(), "cache refresh should succeed");
2429
+ let item = cache
2430
+ .lookup(1001, "bad-\u{FFFD}-name")
2431
+ .expect("lossy lookup");
2432
+ assert_eq!(item.name, "bad-\u{FFFD}-name");
2433
+ assert_eq!(item.path, "/bad/\u{FFFD}/path");
2434
+
2435
+ cache.close();
2436
+ server.stop();
2437
+ cleanup_all(svc);
2438
+}
2439
+
2440
+#[test]
2441
+fn test_cache_refresh_preserves_old_cache_on_malformed_snapshot_item() {
2442
+ let svc = "rs_cache_preserve_bad_item";
2443
+ ensure_run_dir();
2444
+ cleanup_all(svc);
2445
+
2446
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2447
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
2448
+
2449
+ assert!(cache.refresh(), "initial refresh should succeed");
2450
+ let old_status = cache.status();
2451
+ let old_item = cache
2452
+ .lookup(1001, "docker-abc123")
2453
+ .expect("existing cache item")
2454
+ .clone();
2455
+
2456
+ server.stop();
2457
+ cleanup_all(svc);
2458
+
2459
+ let mut raw_server =
2460
+ start_raw_session_server(svc, server_config(), move |session, req_hdr, payload| {
2461
+ let req = crate::protocol::CgroupsRequest::decode(payload)
2462
+ .map_err(|e| format!("decode request: {e:?}"))?;
2463
+ if req.layout_version != 1 || req.flags != 0 {
2464
+ return Err("unexpected snapshot request".into());
2465
+ }
2466
+
2467
+ let mut response_payload = [0u8; 512];
2468
+ let response_len = {
2469
+ let mut builder = CgroupsBuilder::new(&mut response_payload, 1, 1, 99);
2470
+ builder
2471
+ .add(9999, 0, 1, b"new-item", b"/new/path")
2472
+ .map_err(|e| format!("builder add: {e:?}"))?;
2473
+ builder.finish()
2474
+ };
2475
+
2476
+ let dir_end = 24 + 8;
2477
+ let item_off =
2478
+ u32::from_ne_bytes(response_payload[24..28].try_into().unwrap()) as usize;
2479
+ let item_start = dir_end + item_off;
2480
+ response_payload[item_start..item_start + 2].copy_from_slice(&99u16.to_ne_bytes());
2481
+
2482
+ let mut resp_hdr = Header {
2483
+ kind: KIND_RESPONSE,
2484
+ code: METHOD_CGROUPS_SNAPSHOT,
2485
+ flags: 0,
2486
+ item_count: 1,
2487
+ message_id: req_hdr.message_id,
2488
+ transport_status: STATUS_OK,
2489
+ ..Header::default()
2490
+ };
2491
+ session
2492
+ .send(&mut resp_hdr, &response_payload[..response_len])
2493
+ .map_err(|e| format!("send: {e}"))
2494
+ });
2495
+
2496
+ assert!(
2497
+ !cache.refresh(),
2498
+ "malformed snapshot item should preserve the old cache"
2499
+ );
2500
+
2501
+ let status = cache.status();
2502
+ assert!(cache.ready(), "cache should stay populated");
2503
+ assert_eq!(status.item_count, old_status.item_count);
2504
+ assert_eq!(status.generation, old_status.generation);
2505
+ assert_eq!(
2506
+ status.refresh_success_count,
2507
+ old_status.refresh_success_count
2508
+ );
2509
+ assert_eq!(
2510
+ status.refresh_failure_count,
2511
+ old_status.refresh_failure_count + 1
2512
+ );
2513
+ let preserved = cache
2514
+ .lookup(1001, "docker-abc123")
2515
+ .expect("old cache item should remain");
2516
+ assert_eq!(preserved.hash, old_item.hash);
2517
+ assert_eq!(preserved.path, old_item.path);
2518
+ assert!(
2519
+ cache.lookup(9999, "new-item").is_none(),
2520
+ "bad refresh must not replace the old cache"
2521
+ );
2522
+
2523
+ cache.close();
2524
+ raw_server.wait();
2525
+ cleanup_all(svc);
2526
+}
2527
+
2528
+// ---------------------------------------------------------------
2529
+// Stress tests (Phase H4)
2530
+// ---------------------------------------------------------------
2531
+
2532
+/// djb2 hash matching the C implementation
2533
+fn simple_hash(s: &str) -> u32 {
2534
+ let mut hash: u32 = 5381;
2535
+ for c in s.bytes() {
2536
+ hash = hash
2537
+ .wrapping_shl(5)
2538
+ .wrapping_add(hash)
2539
+ .wrapping_add(c as u32);
2540
+ }
2541
+ hash
2542
+}
2543
+
2544
+struct StressTestServer {
2545
+ stop_flag: Arc<AtomicBool>,
2546
+ thread: Option<thread::JoinHandle<()>>,
2547
+}
2548
+
2549
+impl StressTestServer {
2550
+ fn start(service: &str, n: u32, resp_buf_size: usize) -> Self {
2551
+ ensure_run_dir();
2552
+ cleanup_all(service);
2553
+
2554
+ let svc = service.to_string();
2555
+ let ready_flag = Arc::new(AtomicBool::new(false));
2556
+ let ready_clone = ready_flag.clone();
2557
+
2558
+ let mut scfg = server_config();
2559
+ scfg.max_response_payload_bytes = resp_buf_size as u32;
2560
+ scfg.packet_size = 65536; // force smaller packets for chunked transport
2561
+
2562
+ let handler = snapshot_dispatch(
2563
+ Arc::new(move |req, builder| {
2564
+ if req.layout_version != 1 || req.flags != 0 {
2565
+ return false;
2566
+ }
2567
+ builder.set_header(1, 42);
2568
+
2569
+ for i in 0..n {
2570
+ let name = format!("container-{i:04}");
2571
+ let path = format!("/sys/fs/cgroup/docker/{i:04}");
2572
+ let hash = simple_hash(&name);
2573
+ let enabled = if i % 5 == 0 { 0 } else { 1 };
2574
+ if builder
2575
+ .add(hash, 0x10, enabled, name.as_bytes(), path.as_bytes())
2576
+ .is_err()
2577
+ {
2578
+ return false;
2579
+ }
2580
+ }
2581
+
2582
+ true
2583
+ }),
2584
+ n,
2585
+ );
2586
+
2587
+ let mut server = ManagedServer::new(
2588
+ TEST_RUN_DIR,
2589
+ &svc,
2590
+ scfg,
2591
+ METHOD_CGROUPS_SNAPSHOT,
2592
+ Some(handler),
2593
+ );
2594
+ let stop_flag = server.running_flag();
2595
+
2596
+ let thread = thread::spawn(move || {
2597
+ ready_clone.store(true, Ordering::Release);
2598
+ let _ = server.run();
2599
+ });
2600
+
2601
+ for _ in 0..2000 {
2602
+ if ready_flag.load(Ordering::Acquire) {
2603
+ break;
2604
+ }
2605
+ thread::sleep(Duration::from_micros(500));
2606
+ }
2607
+ thread::sleep(Duration::from_millis(50));
2608
+
2609
+ StressTestServer {
2610
+ stop_flag,
2611
+ thread: Some(thread),
2612
+ }
2613
+ }
2614
+
2615
+ fn stop(&mut self) {
2616
+ self.stop_flag.store(false, Ordering::Release);
2617
+ if let Some(t) = self.thread.take() {
2618
+ let _ = t.join();
2619
+ }
2620
+ }
2621
+}
2622
+
2623
+impl Drop for StressTestServer {
2624
+ fn drop(&mut self) {
2625
+ self.stop();
2626
+ }
2627
+}
2628
+
2629
+#[test]
2630
+fn test_stress_1000_items() {
2631
+ let svc = "rs_stress_1k";
2632
+
2633
+ const N: u32 = 1000;
2634
+ const BUF_SIZE: usize = 300 * N as usize;
2635
+
2636
+ let mut server = StressTestServer::start(svc, N, BUF_SIZE);
2637
+
2638
+ let mut cfg = client_config();
2639
+ cfg.max_response_payload_bytes = BUF_SIZE as u32;
2640
+ cfg.packet_size = 65536;
2641
+
2642
+ let mut client = snapshot_client(svc, cfg);
2643
+ client.refresh();
2644
+ assert!(client.ready(), "client not ready");
2645
+
2646
+ let start = std::time::Instant::now();
2647
+ let view = client.call_snapshot().expect("call should succeed");
2648
+ let elapsed = start.elapsed();
2649
+
2650
+ eprintln!(" 1000 items: {:?}", elapsed);
2651
+
2652
+ assert_eq!(view.item_count, N);
2653
+ assert_eq!(view.systemd_enabled, 1);
2654
+ assert_eq!(view.generation, 42);
2655
+
2656
+ // Verify ALL items
2657
+ for i in 0..N {
2658
+ let item = view
2659
+ .item(i)
2660
+ .unwrap_or_else(|_| panic!("item {i} decode failed"));
2661
+ let expected_name = format!("container-{i:04}");
2662
+ let expected_path = format!("/sys/fs/cgroup/docker/{i:04}");
2663
+ let expected_hash = simple_hash(&expected_name);
2664
+ let expected_enabled = if i % 5 == 0 { 0 } else { 1 };
2665
+
2666
+ assert_eq!(item.hash, expected_hash, "item {i} hash mismatch");
2667
+ assert_eq!(
2668
+ std::str::from_utf8(item.name.as_bytes()).unwrap(),
2669
+ expected_name,
2670
+ "item {i} name mismatch"
2671
+ );
2672
+ assert_eq!(
2673
+ std::str::from_utf8(item.path.as_bytes()).unwrap(),
2674
+ expected_path,
2675
+ "item {i} path mismatch"
2676
+ );
2677
+ assert_eq!(item.enabled, expected_enabled, "item {i} enabled mismatch");
2678
+ assert_eq!(item.options, 0x10, "item {i} options mismatch");
2679
+ }
2680
+
2681
+ client.close();
2682
+ server.stop();
2683
+ cleanup_all(svc);
2684
+}
2685
+
2686
+#[test]
2687
+fn test_stress_5000_items() {
2688
+ let svc = "rs_stress_5k";
2689
+
2690
+ const N: u32 = 5000;
2691
+ const BUF_SIZE: usize = 300 * N as usize;
2692
+
2693
+ let mut server = StressTestServer::start(svc, N, BUF_SIZE);
2694
+
2695
+ let mut cfg = client_config();
2696
+ cfg.max_response_payload_bytes = BUF_SIZE as u32;
2697
+ cfg.packet_size = 65536;
2698
+
2699
+ let mut client = snapshot_client(svc, cfg);
2700
+ client.refresh();
2701
+ assert!(client.ready(), "client not ready");
2702
+
2703
+ let start = std::time::Instant::now();
2704
+ let view = client.call_snapshot().expect("call should succeed");
2705
+ let elapsed = start.elapsed();
2706
+
2707
+ eprintln!(" 5000 items: {:?}", elapsed);
2708
+
2709
+ assert_eq!(view.item_count, N);
2710
+
2711
+ // Spot-check first, middle, last
2712
+ for idx in [0, N / 2, N - 1] {
2713
+ let item = view.item(idx).unwrap();
2714
+ let expected_name = format!("container-{idx:04}");
2715
+ let expected_hash = simple_hash(&expected_name);
2716
+ assert_eq!(item.hash, expected_hash);
2717
+ assert_eq!(
2718
+ std::str::from_utf8(item.name.as_bytes()).unwrap(),
2719
+ expected_name
2720
+ );
2721
+ }
2722
+
2723
+ client.close();
2724
+ server.stop();
2725
+ cleanup_all(svc);
2726
+}
2727
+
2728
+#[test]
2729
+fn test_stress_concurrent_clients() {
2730
+ let svc = "rs_stress_concurrent";
2731
+ ensure_run_dir();
2732
+ cleanup_all(svc);
2733
+
2734
+ let mut server = TestServer::start_with_workers(
2735
+ svc,
2736
+ METHOD_CGROUPS_SNAPSHOT,
2737
+ Some(test_cgroups_dispatch()),
2738
+ 64,
2739
+ );
2740
+
2741
+ const NUM_CLIENTS: usize = 50;
2742
+ const REQUESTS_PER: usize = 10;
2743
+
2744
+ let start = std::time::Instant::now();
2745
+
2746
+ let mut handles = Vec::new();
2747
+ for client_id in 0..NUM_CLIENTS {
2748
+ let svc_name = svc.to_string();
2749
+ let handle = thread::spawn(move || {
2750
+ let mut client = snapshot_client(&svc_name, client_config());
2751
+
2752
+ for _ in 0..200 {
2753
+ client.refresh();
2754
+ if client.ready() {
2755
+ break;
2756
+ }
2757
+ thread::sleep(Duration::from_millis(5));
2758
+ }
2759
+
2760
+ assert!(client.ready(), "client {client_id} not ready");
2761
+
2762
+ let mut successes = 0usize;
2763
+ for _ in 0..REQUESTS_PER {
2764
+ match client.call_snapshot() {
2765
+ Ok(view) => {
2766
+ assert_eq!(view.item_count, 3);
2767
+ assert_eq!(view.generation, 42);
2768
+ let item0 = view.item(0).expect("item 0");
2769
+ assert_eq!(item0.hash, 1001);
2770
+ assert_eq!(
2771
+ std::str::from_utf8(item0.name.as_bytes()).unwrap(),
2772
+ "docker-abc123"
2773
+ );
2774
+ let item2 = view.item(2).expect("item 2");
2775
+ assert_eq!(item2.hash, 3003);
2776
+ successes += 1;
2777
+ }
2778
+ Err(e) => panic!("client {client_id} call failed: {:?}", e),
2779
+ }
2780
+ }
2781
+ client.close();
2782
+ successes
2783
+ });
2784
+ handles.push(handle);
2785
+ }
2786
+
2787
+ let mut total = 0usize;
2788
+ for h in handles {
2789
+ total += h.join().expect("client thread panicked");
2790
+ }
2791
+
2792
+ let elapsed = start.elapsed();
2793
+ eprintln!(
2794
+ " {NUM_CLIENTS} clients x {REQUESTS_PER} req: {total}/{} in {:?}",
2795
+ NUM_CLIENTS * REQUESTS_PER,
2796
+ elapsed
2797
+ );
2798
+
2799
+ assert_eq!(total, NUM_CLIENTS * REQUESTS_PER);
2800
+
2801
+ server.stop();
2802
+ cleanup_all(svc);
2803
+}
2804
+
2805
+#[test]
2806
+fn test_stress_rapid_connect_disconnect() {
2807
+ let svc = "rs_stress_rapid";
2808
+ ensure_run_dir();
2809
+ cleanup_all(svc);
2810
+
2811
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2812
+
2813
+ const CYCLES: usize = 1000;
2814
+ let mut successes = 0usize;
2815
+ let mut failures = 0usize;
2816
+
2817
+ let start = std::time::Instant::now();
2818
+
2819
+ for _ in 0..CYCLES {
2820
+ let mut client = snapshot_client(svc, client_config());
2821
+
2822
+ let mut connected = false;
2823
+ /* Under full ctest -j load, a freshly spawned server may need a
2824
+ * slightly longer connect window than the hot-path unit tests. */
2825
+ for _ in 0..200 {
2826
+ client.refresh();
2827
+ if client.ready() {
2828
+ connected = true;
2829
+ break;
2830
+ }
2831
+ thread::sleep(Duration::from_millis(2));
2832
+ }
2833
+
2834
+ if !connected {
2835
+ failures += 1;
2836
+ client.close();
2837
+ continue;
2838
+ }
2839
+
2840
+ match client.call_snapshot() {
2841
+ Ok(view) => {
2842
+ if view.item_count == 3 && view.generation == 42 {
2843
+ successes += 1;
2844
+ } else {
2845
+ failures += 1;
2846
+ }
2847
+ }
2848
+ Err(_) => failures += 1,
2849
+ }
2850
+
2851
+ client.close();
2852
+ }
2853
+
2854
+ let elapsed = start.elapsed();
2855
+ eprintln!(
2856
+ " {CYCLES} rapid cycles: {successes} ok, {failures} fail, {:?}",
2857
+ elapsed
2858
+ );
2859
+
2860
+ assert_eq!(successes, CYCLES, "all cycles should succeed");
2861
+ assert_eq!(failures, 0, "no failures expected");
2862
+
2863
+ server.stop();
2864
+ cleanup_all(svc);
2865
+}
2866
+
2867
+#[test]
2868
+fn test_stress_cache_concurrent() {
2869
+ let svc = "rs_stress_cache";
2870
+ ensure_run_dir();
2871
+ cleanup_all(svc);
2872
+
2873
+ let mut server = TestServer::start_with_workers(
2874
+ svc,
2875
+ METHOD_CGROUPS_SNAPSHOT,
2876
+ Some(test_cgroups_dispatch()),
2877
+ 16,
2878
+ );
2879
+
2880
+ const NUM_CLIENTS: usize = 10;
2881
+ const REQUESTS_PER: usize = 100;
2882
+
2883
+ let start = std::time::Instant::now();
2884
+
2885
+ let mut handles = Vec::new();
2886
+ for _ in 0..NUM_CLIENTS {
2887
+ let svc_name = svc.to_string();
2888
+ let handle = thread::spawn(move || {
2889
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, &svc_name, client_config());
2890
+ let mut successes = 0usize;
2891
+
2892
+ for _ in 0..REQUESTS_PER {
2893
+ let updated = cache.refresh();
2894
+ if updated || cache.ready() {
2895
+ let status = cache.status();
2896
+ if status.item_count != 3 {
2897
+ continue;
2898
+ }
2899
+ let item = cache.lookup(1001, "docker-abc123");
2900
+ if item.is_some() && item.unwrap().hash == 1001 {
2901
+ successes += 1;
2902
+ }
2903
+ }
2904
+ }
2905
+ cache.close();
2906
+ successes
2907
+ });
2908
+ handles.push(handle);
2909
+ }
2910
+
2911
+ let mut total = 0usize;
2912
+ for h in handles {
2913
+ total += h.join().expect("cache thread panicked");
2914
+ }
2915
+
2916
+ let elapsed = start.elapsed();
2917
+ eprintln!(
2918
+ " {NUM_CLIENTS} cache clients x {REQUESTS_PER} req: {total}/{} in {:?}",
2919
+ NUM_CLIENTS * REQUESTS_PER,
2920
+ elapsed
2921
+ );
2922
+
2923
+ assert_eq!(total, NUM_CLIENTS * REQUESTS_PER);
2924
+
2925
+ server.stop();
2926
+ cleanup_all(svc);
2927
+}
2928
+
2929
+#[test]
2930
+fn test_stress_long_running() {
2931
+ let svc = "rs_stress_long";
2932
+ ensure_run_dir();
2933
+ cleanup_all(svc);
2934
+
2935
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
2936
+
2937
+ const NUM_CLIENTS: usize = 5;
2938
+ let run_duration = Duration::from_secs(30);
2939
+
2940
+ let running = Arc::new(AtomicBool::new(true));
2941
+ let mut handles = Vec::new();
2942
+
2943
+ for _ in 0..NUM_CLIENTS {
2944
+ let svc_name = svc.to_string();
2945
+ let r = running.clone();
2946
+ let handle = thread::spawn(move || {
2947
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, &svc_name, client_config());
2948
+ let mut refreshes = 0u64;
2949
+ let mut errors = 0u64;
2950
+
2951
+ while r.load(Ordering::Acquire) {
2952
+ let updated = cache.refresh();
2953
+ if updated || cache.ready() {
2954
+ let status = cache.status();
2955
+ if status.item_count == 3 {
2956
+ refreshes += 1;
2957
+ } else {
2958
+ errors += 1;
2959
+ }
2960
+ } else {
2961
+ errors += 1;
2962
+ }
2963
+ thread::sleep(Duration::from_millis(1));
2964
+ }
2965
+
2966
+ cache.close();
2967
+ (refreshes, errors)
2968
+ });
2969
+ handles.push(handle);
2970
+ }
2971
+
2972
+ thread::sleep(run_duration);
2973
+ running.store(false, Ordering::Release);
2974
+
2975
+ let mut total_refreshes = 0u64;
2976
+ let mut total_errors = 0u64;
2977
+ for h in handles {
2978
+ let (r, e) = h.join().expect("client thread panicked");
2979
+ total_refreshes += r;
2980
+ total_errors += e;
2981
+ }
2982
+
2983
+ eprintln!(" 30s run: {total_refreshes} refreshes, {total_errors} errors");
2984
+
2985
+ assert!(total_refreshes > 0, "expected some refreshes");
2986
+ assert_eq!(total_errors, 0, "expected zero errors in 60s run");
2987
+
2988
+ server.stop();
2989
+ cleanup_all(svc);
2990
+}
2991
+
2992
+// ---------------------------------------------------------------
2993
+// Ping-pong tests per service kind
2994
+// ---------------------------------------------------------------
2995
+
2996
+#[test]
2997
+fn test_increment_ping_pong() {
2998
+ let svc = "rs_pp_incr";
2999
+ ensure_run_dir();
3000
+ cleanup_all(svc);
3001
+
3002
+ let mut server = TestServer::start(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
3003
+
3004
+ let mut client = increment_client(svc, client_config());
3005
+ client.refresh();
3006
+ assert!(client.ready(), "client not ready");
3007
+
3008
+ // Ping-pong: send 0 -> get 1 -> send 1 -> get 2 -> ... -> 10
3009
+ let mut value = 0u64;
3010
+ let mut responses_received = 0u64;
3011
+ for round in 0..10 {
3012
+ let sent = value;
3013
+ let result = client
3014
+ .call_increment(sent)
3015
+ .unwrap_or_else(|e| panic!("round {round}: call_increment({sent}) failed: {e:?}"));
3016
+ assert_eq!(
3017
+ result,
3018
+ sent + 1,
3019
+ "round {round}: expected {} got {result}",
3020
+ sent + 1
3021
+ );
3022
+ responses_received += 1;
3023
+ value = result;
3024
+ }
3025
+ assert_eq!(
3026
+ responses_received, 10,
3027
+ "expected 10 responses, got {responses_received}"
3028
+ );
3029
+ assert_eq!(value, 10, "final value after 10 rounds");
3030
+
3031
+ client.close();
3032
+ server.stop();
3033
+ cleanup_all(svc);
3034
+}
3035
+
3036
+#[test]
3037
+fn test_string_reverse_ping_pong() {
3038
+ let svc = "rs_pp_strrev";
3039
+ ensure_run_dir();
3040
+ cleanup_all(svc);
3041
+
3042
+ let mut server = TestServer::start(
3043
+ svc,
3044
+ METHOD_STRING_REVERSE,
3045
+ Some(string_reverse_dispatch_handler()),
3046
+ );
3047
+
3048
+ let mut client = string_reverse_client(svc, client_config());
3049
+ client.refresh();
3050
+ assert!(client.ready(), "client not ready");
3051
+
3052
+ let original = "abcdefghijklmnopqrstuvwxyz";
3053
+ let mut current = original.to_string();
3054
+ let mut responses_received = 0u64;
3055
+
3056
+ // 6 rounds: feed each response back as next request
3057
+ for round in 0..6 {
3058
+ let sent = current.clone();
3059
+ let expected: String = sent.chars().rev().collect();
3060
+ let result = client.call_string_reverse(&sent).unwrap_or_else(|e| {
3061
+ panic!("round {round}: call_string_reverse({sent:?}) failed: {e:?}")
3062
+ });
3063
+ assert_eq!(
3064
+ result.as_str(),
3065
+ expected,
3066
+ "round {round}: reverse of {sent:?} should be {expected:?}, got {result:?}"
3067
+ );
3068
+ responses_received += 1;
3069
+ current = result.as_str().to_string();
3070
+ }
3071
+ assert_eq!(
3072
+ responses_received, 6,
3073
+ "expected 6 responses, got {responses_received}"
3074
+ );
3075
+ // even number of reversals = identity
3076
+ assert_eq!(
3077
+ current, original,
3078
+ "6 reversals should restore original string"
3079
+ );
3080
+
3081
+ client.close();
3082
+ server.stop();
3083
+ cleanup_all(svc);
3084
+}
3085
+
3086
+#[test]
3087
+fn test_increment_batch() {
3088
+ let svc = "rs_pp_batch";
3089
+ ensure_run_dir();
3090
+ cleanup_all(svc);
3091
+
3092
+ // Need batch items > 1 for both client and server configs
3093
+ fn batch_server_config() -> ServerConfig {
3094
+ ServerConfig {
3095
+ supported_profiles: PROFILE_BASELINE,
3096
+ max_request_payload_bytes: 4096,
3097
+ max_request_batch_items: 16,
3098
+ max_response_payload_bytes: 4096,
3099
+ max_response_batch_items: 16,
3100
+ auth_token: AUTH_TOKEN,
3101
+ backlog: 4,
3102
+ ..ServerConfig::default()
3103
+ }
3104
+ }
3105
+
3106
+ fn batch_client_config() -> ClientConfig {
3107
+ ClientConfig {
3108
+ supported_profiles: PROFILE_BASELINE,
3109
+ max_request_payload_bytes: 4096,
3110
+ max_request_batch_items: 16,
3111
+ max_response_payload_bytes: 4096,
3112
+ max_response_batch_items: 16,
3113
+ auth_token: AUTH_TOKEN,
3114
+ ..ClientConfig::default()
3115
+ }
3116
+ }
3117
+
3118
+ // Start server with batch-capable config
3119
+ let svc_name = svc.to_string();
3120
+ let ready_flag = Arc::new(AtomicBool::new(false));
3121
+ let ready_clone = ready_flag.clone();
3122
+
3123
+ let mut server_obj = ManagedServer::with_workers(
3124
+ TEST_RUN_DIR,
3125
+ &svc_name,
3126
+ batch_server_config(),
3127
+ METHOD_INCREMENT,
3128
+ Some(increment_dispatch_handler()),
3129
+ 8,
3130
+ );
3131
+ let stop_flag = server_obj.running_flag();
3132
+
3133
+ let thread_handle = thread::spawn(move || {
3134
+ ready_clone.store(true, Ordering::Release);
3135
+ let _ = server_obj.run();
3136
+ });
3137
+
3138
+ for _ in 0..2000 {
3139
+ if ready_flag.load(Ordering::Acquire) {
3140
+ break;
3141
+ }
3142
+ thread::sleep(Duration::from_micros(500));
3143
+ }
3144
+ thread::sleep(Duration::from_millis(50));
3145
+
3146
+ let mut client = increment_client(svc, batch_client_config());
3147
+ client.refresh();
3148
+ assert!(client.ready(), "client not ready");
3149
+
3150
+ // Send batch of [10, 20, 30, 40, 50]
3151
+ let values = vec![10u64, 20, 30, 40, 50];
3152
+ let results = client.call_increment_batch(&values).expect("batch call");
3153
+
3154
+ assert_eq!(results.len(), 5);
3155
+ for (i, (&input, &output)) in values.iter().zip(results.iter()).enumerate() {
3156
+ assert_eq!(
3157
+ output,
3158
+ input + 1,
3159
+ "batch item {i}: expected {}, got {output}",
3160
+ input + 1
3161
+ );
3162
+ }
3163
+
3164
+ // Single item batch
3165
+ let single = client
3166
+ .call_increment_batch(&[99])
3167
+ .expect("single-item batch");
3168
+ assert_eq!(single, vec![100]);
3169
+
3170
+ // Empty batch
3171
+ let empty = client.call_increment_batch(&[]).expect("empty batch");
3172
+ assert!(empty.is_empty());
3173
+
3174
+ client.close();
3175
+ stop_flag.store(false, Ordering::Release);
3176
+ let _ = thread_handle.join();
3177
+ cleanup_all(svc);
3178
+}
3179
+
3180
+// ---------------------------------------------------------------
3181
+// Client state machine: auth failure (lines 438-440)
3182
+// ---------------------------------------------------------------
3183
+
3184
+#[test]
3185
+fn test_client_auth_failure() {
3186
+ let svc = "rs_svc_authfail";
3187
+ ensure_run_dir();
3188
+ cleanup_all(svc);
3189
+
3190
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
3191
+
3192
+ // Client with wrong auth token
3193
+ let mut bad_cfg = client_config();
3194
+ bad_cfg.auth_token = 0xBAD_BAD_BAD;
3195
+
3196
+ let mut client = snapshot_client(svc, bad_cfg);
3197
+ client.refresh();
3198
+ assert_eq!(client.state, ClientState::AuthFailed);
3199
+ assert!(!client.ready());
3200
+
3201
+ // Subsequent refresh stays stuck in AuthFailed
3202
+ client.refresh();
3203
+ assert_eq!(client.state, ClientState::AuthFailed);
3204
+
3205
+ client.close();
3206
+ server.stop();
3207
+ cleanup_all(svc);
3208
+}
3209
+
3210
+// ---------------------------------------------------------------
3211
+// Client state machine: incompatible (lines 439-440)
3212
+// ---------------------------------------------------------------
3213
+
3214
+#[test]
3215
+fn test_client_incompatible() {
3216
+ let svc = "rs_svc_incompat";
3217
+ ensure_run_dir();
3218
+ cleanup_all(svc);
3219
+
3220
+ // Server supports only PROFILE_BASELINE, but start it first
3221
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
3222
+
3223
+ // Client requires SHM_FUTEX only (no baseline)
3224
+ let mut bad_cfg = client_config();
3225
+ #[cfg(target_os = "linux")]
3226
+ {
3227
+ bad_cfg.supported_profiles = crate::protocol::PROFILE_SHM_FUTEX;
3228
+ }
3229
+ #[cfg(not(target_os = "linux"))]
3230
+ {
3231
+ // On non-Linux, use a profile bit that won't match
3232
+ bad_cfg.supported_profiles = 0x80000000;
3233
+ }
3234
+
3235
+ let mut client = snapshot_client(svc, bad_cfg);
3236
+ client.refresh();
3237
+ assert_eq!(client.state, ClientState::Incompatible);
3238
+ assert!(!client.ready());
3239
+
3240
+ // Stays stuck
3241
+ client.refresh();
3242
+ assert_eq!(client.state, ClientState::Incompatible);
3243
+
3244
+ client.close();
3245
+ server.stop();
3246
+ cleanup_all(svc);
3247
+}
3248
+
3249
+#[test]
3250
+fn test_client_protocol_version_incompatible() {
3251
+ let svc = unique_service("rs_svc_proto_incompat");
3252
+ ensure_run_dir();
3253
+ cleanup_all(&svc);
3254
+
3255
+ let packet =
3256
+ hello_ack_packet_with_version(crate::protocol::VERSION + 1, crate::protocol::STATUS_OK, 1);
3257
+ let mut server = start_raw_hello_ack_server(&svc, packet);
3258
+
3259
+ let mut client = snapshot_client(&svc, client_config());
3260
+ let changed = client.refresh();
3261
+ assert!(changed, "refresh should move client into INCOMPATIBLE");
3262
+ assert_eq!(client.state, ClientState::Incompatible);
3263
+ assert!(!client.ready());
3264
+
3265
+ server.wait();
3266
+
3267
+ let changed = client.refresh();
3268
+ assert!(!changed, "refresh from incompatible should be a no-op");
3269
+ assert_eq!(client.state, ClientState::Incompatible);
3270
+
3271
+ client.close();
3272
+ cleanup_all(&svc);
3273
+}
3274
+
3275
+// ---------------------------------------------------------------
3276
+// Client: call_snapshot when not ready (line 324-326)
3277
+// ---------------------------------------------------------------
3278
+
3279
+#[test]
3280
+fn test_call_when_not_ready() {
3281
+ let svc = "rs_svc_noready";
3282
+ ensure_run_dir();
3283
+ cleanup_all(svc);
3284
+
3285
+ let mut snapshot = snapshot_client(svc, client_config());
3286
+ assert_eq!(snapshot.state, ClientState::Disconnected);
3287
+ assert!(snapshot.call_snapshot().is_err());
3288
+ assert_eq!(snapshot.status().error_count, 1);
3289
+ snapshot.close();
3290
+
3291
+ let mut increment = increment_client(svc, client_config());
3292
+ assert_eq!(increment.state, ClientState::Disconnected);
3293
+ assert!(increment.call_increment(42).is_err());
3294
+ assert!(increment.call_increment_batch(&[1, 2]).is_err());
3295
+ assert_eq!(increment.status().error_count, 2);
3296
+ increment.close();
3297
+
3298
+ let mut string_reverse = string_reverse_client(svc, client_config());
3299
+ assert_eq!(string_reverse.state, ClientState::Disconnected);
3300
+ assert!(string_reverse.call_string_reverse("test").is_err());
3301
+ assert_eq!(string_reverse.status().error_count, 1);
3302
+ string_reverse.close();
3303
+
3304
+ cleanup_all(svc);
3305
+}
3306
+
3307
+#[test]
3308
+fn test_client_invalid_service_name_maps_to_disconnected() {
3309
+ let bad_service = "x".repeat(400);
3310
+ let mut client = snapshot_client(&bad_service, client_config());
3311
+
3312
+ client.refresh();
3313
+
3314
+ assert_eq!(client.state, ClientState::Disconnected);
3315
+ assert!(!client.ready());
3316
+}
3317
+
3318
+// ---------------------------------------------------------------
3319
+// Client: broken -> reconnect cycle (lines 136-141)
3320
+// ---------------------------------------------------------------
3321
+
3322
+#[test]
3323
+fn test_broken_reconnect() {
3324
+ let svc = "rs_svc_broken";
3325
+ ensure_run_dir();
3326
+ cleanup_all(svc);
3327
+
3328
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
3329
+
3330
+ let mut client = snapshot_client(svc, client_config());
3331
+ client.refresh();
3332
+ assert_eq!(client.state, ClientState::Ready);
3333
+
3334
+ // Force broken state
3335
+ client.state = ClientState::Broken;
3336
+
3337
+ // refresh from Broken should disconnect, reconnect
3338
+ let changed = client.refresh();
3339
+ assert!(changed);
3340
+ assert_eq!(client.state, ClientState::Ready);
3341
+ assert!(client.status().reconnect_count >= 1);
3342
+
3343
+ client.close();
3344
+ server.stop();
3345
+ cleanup_all(svc);
3346
+}
3347
+
3348
+// ---------------------------------------------------------------
3349
+// Cache: close resets everything (line 1561-1566)
3350
+// ---------------------------------------------------------------
3351
+
3352
+#[test]
3353
+fn test_cache_close_resets() {
3354
+ let svc = "rs_cache_close";
3355
+ ensure_run_dir();
3356
+ cleanup_all(svc);
3357
+
3358
+ let mut server = TestServer::start(svc, METHOD_CGROUPS_SNAPSHOT, Some(test_cgroups_dispatch()));
3359
+
3360
+ let mut cache = CgroupsCache::new(TEST_RUN_DIR, svc, client_config());
3361
+ assert!(cache.refresh());
3362
+ assert!(cache.ready());
3363
+ assert_eq!(cache.status().item_count, 3);
3364
+
3365
+ cache.close();
3366
+ assert!(!cache.ready());
3367
+ assert!(cache.lookup(1001, "docker-abc123").is_none());
3368
+ assert_eq!(cache.status().item_count, 0);
3369
+
3370
+ server.stop();
3371
+ cleanup_all(svc);
3372
+}
3373
+
3374
+// ---------------------------------------------------------------
3375
+// Cache: with max_response_payload_bytes = 0 (line 1437-1440)
3376
+// ---------------------------------------------------------------
3377
+
3378
+#[test]
3379
+fn test_cache_default_buf_size() {
3380
+ let svc = "rs_cache_defbuf";
3381
+ ensure_run_dir();
3382
+ cleanup_all(svc);
3383
+
3384
+ // Config with max_response_payload_bytes = 0 triggers default buf
3385
+ let mut cfg = client_config();
3386
+ cfg.max_response_payload_bytes = 0;
3387
+
3388
+ let cache = CgroupsCache::new(TEST_RUN_DIR, svc, cfg);
3389
+ assert_eq!(
3390
+ cache.client.max_receive_message_bytes(),
3391
+ HEADER_SIZE + CACHE_RESPONSE_BUF_SIZE
3392
+ );
3393
+
3394
+ cleanup_all(svc);
3395
+}
3396
+
3397
+// ---------------------------------------------------------------
3398
+// ManagedServer: worker_count = 0 -> clamped to 1 (line 778)
3399
+// ---------------------------------------------------------------
3400
+
3401
+#[test]
3402
+fn test_server_worker_count_clamped() {
3403
+ let svc = "rs_svc_w0";
3404
+ ensure_run_dir();
3405
+ cleanup_all(svc);
3406
+
3407
+ let server = ManagedServer::with_workers(
3408
+ TEST_RUN_DIR,
3409
+ svc,
3410
+ server_config(),
3411
+ METHOD_INCREMENT,
3412
+ None,
3413
+ 0,
3414
+ );
3415
+ assert_eq!(server.worker_count, 1);
3416
+
3417
+ cleanup_all(svc);
3418
+}
3419
+
3420
+// ---------------------------------------------------------------
3421
+// ManagedServer: stop flag (line 946)
3422
+// ---------------------------------------------------------------
3423
+
3424
+#[test]
3425
+fn test_server_stop_flag() {
3426
+ let svc = "rs_svc_stopflag";
3427
+ ensure_run_dir();
3428
+ cleanup_all(svc);
3429
+
3430
+ let server = ManagedServer::new(TEST_RUN_DIR, svc, server_config(), METHOD_INCREMENT, None);
3431
+ let flag = server.running_flag();
3432
+ assert!(!flag.load(Ordering::Acquire));
3433
+
3434
+ // stop sets running to false
3435
+ server.stop();
3436
+ assert!(!flag.load(Ordering::Acquire));
3437
+
3438
+ cleanup_all(svc);
3439
+}
3440
+
3441
+// ---------------------------------------------------------------
3442
+// ClientStatus / CgroupsCacheStatus fields
3443
+// ---------------------------------------------------------------
3444
+
3445
+#[test]
3446
+fn test_client_status_fields() {
3447
+ let svc = "rs_svc_csf";
3448
+ ensure_run_dir();
3449
+ cleanup_all(svc);
3450
+
3451
+ let client = snapshot_client(svc, client_config());
3452
+ let status = client.status();
3453
+ assert_eq!(status.state, ClientState::Disconnected);
3454
+ assert_eq!(status.connect_count, 0);
3455
+ assert_eq!(status.reconnect_count, 0);
3456
+ assert_eq!(status.call_count, 0);
3457
+ assert_eq!(status.error_count, 0);
3458
+ cleanup_all(svc);
3459
+}
3460
+
3461
+// ---------------------------------------------------------------
3462
+// call_increment and call_string_reverse success paths
3463
+// ---------------------------------------------------------------
3464
+
3465
+#[test]
3466
+fn test_client_call_increment_success() {
3467
+ let svc = "rs_svc_incr_ok";
3468
+ ensure_run_dir();
3469
+ cleanup_all(svc);
3470
+
3471
+ let mut server = TestServer::start(svc, METHOD_INCREMENT, Some(increment_dispatch_handler()));
3472
+
3473
+ let mut client = increment_client(svc, client_config());
3474
+ client.refresh();
3475
+ assert!(client.ready());
3476
+
3477
+ let result = client.call_increment(99).expect("increment");
3478
+ assert_eq!(result, 100);
3479
+
3480
+ client.close();
3481
+ server.stop();
3482
+ cleanup_all(svc);
3483
+}
3484
+
3485
+#[test]
3486
+fn test_client_call_string_reverse_success() {
3487
+ let svc = "rs_svc_strrev_ok";
3488
+ ensure_run_dir();
3489
+ cleanup_all(svc);
3490
+
3491
+ let mut server = TestServer::start(
3492
+ svc,
3493
+ METHOD_STRING_REVERSE,
3494
+ Some(string_reverse_dispatch_handler()),
3495
+ );
3496
+
3497
+ let mut client = string_reverse_client(svc, client_config());
3498
+ client.refresh();
3499
+ assert!(client.ready());
3500
+
3501
+ let result = client.call_string_reverse("hello").expect("reverse");
3502
+ assert_eq!(result.as_str(), "olleh");
3503
+
3504
+ client.close();
3505
+ server.stop();
3506
+ cleanup_all(svc);
3507
+}
3508
+
3509
+#[test]
3510
+fn test_dispatch_single_helper_paths() {
3511
+ let mut response_buf = [0u8; 128];
3512
+
3513
+ assert!(matches!(
3514
+ dispatch_single(
3515
+ METHOD_INCREMENT,
3516
+ None,
3517
+ METHOD_INCREMENT,
3518
+ &[0; 8],
3519
+ &mut response_buf,
3520
+ ),
3521
+ Err(_)
3522
+ ));
3523
+
3524
+ assert!(matches!(
3525
+ dispatch_single(
3526
+ METHOD_STRING_REVERSE,
3527
+ None,
3528
+ METHOD_STRING_REVERSE,
3529
+ &[0; 8],
3530
+ &mut response_buf,
3531
+ ),
3532
+ Err(_)
3533
+ ));
3534
+
3535
+ assert!(matches!(
3536
+ dispatch_single(
3537
+ METHOD_CGROUPS_SNAPSHOT,
3538
+ None,
3539
+ METHOD_CGROUPS_SNAPSHOT,
3540
+ &[1, 0, 0, 0],
3541
+ &mut response_buf,
3542
+ ),
3543
+ Err(_)
3544
+ ));
3545
+
3546
+ let snapshot_handler = snapshot_dispatch(Arc::new(|_, _| true), 0);
3547
+ assert!(matches!(
3548
+ dispatch_single(
3549
+ METHOD_CGROUPS_SNAPSHOT,
3550
+ Some(&snapshot_handler),
3551
+ METHOD_CGROUPS_SNAPSHOT,
3552
+ &[1, 0, 0, 0],
3553
+ &mut [],
3554
+ ),
3555
+ Err(_)
3556
+ ));
3557
+
3558
+ let reverse_handler = string_reverse_dispatch_handler();
3559
+ let mut invalid_utf8_req = [0u8; 16];
3560
+ let invalid_len = string_reverse_encode(&[0xff], &mut invalid_utf8_req);
3561
+ let n = dispatch_single(
3562
+ METHOD_STRING_REVERSE,
3563
+ Some(&reverse_handler),
3564
+ METHOD_STRING_REVERSE,
3565
+ &invalid_utf8_req[..invalid_len],
3566
+ &mut response_buf,
3567
+ )
3568
+ .expect("non-UTF8 string_reverse input should decode as an empty string");
3569
+ let view =
3570
+ string_reverse_decode(&response_buf[..n]).expect("decode empty-string reverse response");
3571
+ assert_eq!(view.as_str(), "");
3572
+
3573
+ assert!(matches!(
3574
+ dispatch_single(
3575
+ METHOD_STRING_REVERSE,
3576
+ Some(&reverse_handler),
3577
+ METHOD_INCREMENT,
3578
+ &invalid_utf8_req[..invalid_len],
3579
+ &mut response_buf,
3580
+ ),
3581
+ Err(_)
3582
+ ));
3583
+
3584
+ let snapshot_fail_handler = snapshot_dispatch(Arc::new(|_, _| false), 1);
3585
+ assert!(matches!(
3586
+ dispatch_single(
3587
+ METHOD_CGROUPS_SNAPSHOT,
3588
+ Some(&snapshot_fail_handler),
3589
+ METHOD_CGROUPS_SNAPSHOT,
3590
+ &[1, 0, 0, 0],
3591
+ &mut response_buf,
3592
+ ),
3593
+ Err(_)
3594
+ ));
3595
+
3596
+ assert!(matches!(
3597
+ dispatch_single(0xFFFF, None, 0xFFFF, &[], &mut response_buf),
3598
+ Err(_)
3599
+ ));
3600
+
3601
+ assert!(
3602
+ snapshot_max_items(4096, 0) > 0,
3603
+ "default snapshot item estimate should be positive for a non-empty buffer"
3604
+ );
3605
+}
3606
+
3607
+#[test]
3608
+fn test_response_payload_transport_buf_bounds() {
3609
+ let mut client = snapshot_client("rs_payload_bounds", client_config());
3610
+ client.transport_buf.resize(HEADER_SIZE + 8, 0);
3611
+
3612
+ let response = ClientResponseRef {
3613
+ source: ClientResponseSource::TransportBuf,
3614
+ len: 16,
3615
+ };
3616
+
3617
+ assert_eq!(client.response_payload(response), Err(NipcError::Truncated));
3618
+}
3619
+
3620
+#[test]
3621
+fn test_call_increment_rejects_malformed_response_envelope_unix() {
3622
+ struct Case {
3623
+ name: &'static str,
3624
+ kind: u16,
3625
+ code: u16,
3626
+ status: u16,
3627
+ message_id_delta: u64,
3628
+ want: NipcError,
3629
+ }
3630
+
3631
+ let cases = [
3632
+ Case {
3633
+ name: "bad kind",
3634
+ kind: KIND_REQUEST,
3635
+ code: METHOD_INCREMENT,
3636
+ status: STATUS_OK,
3637
+ message_id_delta: 0,
3638
+ want: NipcError::BadKind,
3639
+ },
3640
+ Case {
3641
+ name: "bad code",
3642
+ kind: KIND_RESPONSE,
3643
+ code: METHOD_STRING_REVERSE,
3644
+ status: STATUS_OK,
3645
+ message_id_delta: 0,
3646
+ want: NipcError::BadLayout,
3647
+ },
3648
+ Case {
3649
+ name: "bad status",
3650
+ kind: KIND_RESPONSE,
3651
+ code: METHOD_INCREMENT,
3652
+ status: STATUS_INTERNAL_ERROR,
3653
+ message_id_delta: 0,
3654
+ want: NipcError::BadLayout,
3655
+ },
3656
+ Case {
3657
+ name: "bad message id",
3658
+ kind: KIND_RESPONSE,
3659
+ code: METHOD_INCREMENT,
3660
+ status: STATUS_OK,
3661
+ message_id_delta: 1,
3662
+ want: NipcError::Truncated,
3663
+ },
3664
+ ];
3665
+
3666
+ for tc in cases {
3667
+ let svc = format!("rs_unix_inc_env_{}", tc.name.replace(' ', "_"));
3668
+ let mut server =
3669
+ start_raw_session_server(&svc, server_config(), move |session, req_hdr, _| {
3670
+ let mut payload = [0u8; INCREMENT_PAYLOAD_SIZE];
3671
+ let n = increment_encode(43, &mut payload);
3672
+ if n != INCREMENT_PAYLOAD_SIZE {
3673
+ return Err(format!("increment_encode returned {n}"));
3674
+ }
3675
+
3676
+ let mut resp_hdr = Header {
3677
+ kind: tc.kind,
3678
+ code: tc.code,
3679
+ flags: 0,
3680
+ item_count: 1,
3681
+ message_id: req_hdr.message_id + tc.message_id_delta,
3682
+ transport_status: tc.status,
3683
+ ..Header::default()
3684
+ };
3685
+ session
3686
+ .send(&mut resp_hdr, &payload)
3687
+ .map_err(|e| format!("send: {e}"))
3688
+ });
3689
+
3690
+ let mut client = increment_client(&svc, client_config());
3691
+ connect_ready(&mut client);
3692
+
3693
+ let err = client.call_increment(42).expect_err(tc.name);
3694
+ assert_eq!(err, tc.want, "{}", tc.name);
3695
+
3696
+ client.close();
3697
+ server.wait();
3698
+ cleanup_all(&svc);
3699
+ }
3700
+}
3701
+
3702
+#[test]
3703
+fn test_call_string_reverse_rejects_malformed_response_envelope_unix() {
3704
+ struct Case {
3705
+ name: &'static str,
3706
+ kind: u16,
3707
+ code: u16,
3708
+ status: u16,
3709
+ message_id_delta: u64,
3710
+ want: NipcError,
3711
+ }
3712
+
3713
+ let cases = [
3714
+ Case {
3715
+ name: "bad kind",
3716
+ kind: KIND_REQUEST,
3717
+ code: METHOD_STRING_REVERSE,
3718
+ status: STATUS_OK,
3719
+ message_id_delta: 0,
3720
+ want: NipcError::BadKind,
3721
+ },
3722
+ Case {
3723
+ name: "bad code",
3724
+ kind: KIND_RESPONSE,
3725
+ code: METHOD_INCREMENT,
3726
+ status: STATUS_OK,
3727
+ message_id_delta: 0,
3728
+ want: NipcError::BadLayout,
3729
+ },
3730
+ Case {
3731
+ name: "bad status",
3732
+ kind: KIND_RESPONSE,
3733
+ code: METHOD_STRING_REVERSE,
3734
+ status: STATUS_INTERNAL_ERROR,
3735
+ message_id_delta: 0,
3736
+ want: NipcError::BadLayout,
3737
+ },
3738
+ Case {
3739
+ name: "bad message id",
3740
+ kind: KIND_RESPONSE,
3741
+ code: METHOD_STRING_REVERSE,
3742
+ status: STATUS_OK,
3743
+ message_id_delta: 1,
3744
+ want: NipcError::Truncated,
3745
+ },
3746
+ ];
3747
+
3748
+ for tc in cases {
3749
+ let svc = format!("rs_unix_str_env_{}", tc.name.replace(' ', "_"));
3750
+ let mut server =
3751
+ start_raw_session_server(&svc, server_config(), move |session, req_hdr, _| {
3752
+ let mut payload = [0u8; 128];
3753
+ let n = string_reverse_encode(b"olleh", &mut payload);
3754
+ if n == 0 {
3755
+ return Err("string_reverse_encode returned 0".into());
3756
+ }
3757
+
3758
+ let mut resp_hdr = Header {
3759
+ kind: tc.kind,
3760
+ code: tc.code,
3761
+ flags: 0,
3762
+ item_count: 1,
3763
+ message_id: req_hdr.message_id + tc.message_id_delta,
3764
+ transport_status: tc.status,
3765
+ ..Header::default()
3766
+ };
3767
+ session
3768
+ .send(&mut resp_hdr, &payload[..n])
3769
+ .map_err(|e| format!("send: {e}"))
3770
+ });
3771
+
3772
+ let mut client = string_reverse_client(&svc, client_config());
3773
+ connect_ready(&mut client);
3774
+
3775
+ let err = client.call_string_reverse("hello").expect_err(tc.name);
3776
+ assert_eq!(err, tc.want, "{}", tc.name);
3777
+
3778
+ client.close();
3779
+ server.wait();
3780
+ cleanup_all(&svc);
3781
+ }
3782
+}
3783
+
3784
+#[test]
3785
+fn test_call_increment_batch_rejects_wrong_item_count_unix() {
3786
+ let svc = "rs_unix_batch_count";
3787
+ ensure_run_dir();
3788
+ cleanup_all(svc);
3789
+
3790
+ let mut server =
3791
+ start_raw_session_server(svc, batch_server_config(), move |session, req_hdr, _| {
3792
+ let mut encoded = [0u8; INCREMENT_PAYLOAD_SIZE];
3793
+ let n = increment_encode(11, &mut encoded);
3794
+ if n != INCREMENT_PAYLOAD_SIZE {
3795
+ return Err(format!("increment_encode returned {n}"));
3796
+ }
3797
+
3798
+ let mut response_buf = vec![0u8; 128];
3799
+ let resp_len = {
3800
+ let mut batch = BatchBuilder::new(&mut response_buf, 2);
3801
+ batch
3802
+ .add(&encoded)
3803
+ .map_err(|e| format!("batch add 1: {e:?}"))?;
3804
+ batch
3805
+ .add(&encoded)
3806
+ .map_err(|e| format!("batch add 2: {e:?}"))?;
3807
+ let (len, _count) = batch.finish();
3808
+ len
3809
+ };
3810
+
3811
+ let mut resp_hdr = Header {
3812
+ kind: KIND_RESPONSE,
3813
+ code: METHOD_INCREMENT,
3814
+ flags: FLAG_BATCH,
3815
+ item_count: 1,
3816
+ message_id: req_hdr.message_id,
3817
+ transport_status: STATUS_OK,
3818
+ ..Header::default()
3819
+ };
3820
+ session
3821
+ .send(&mut resp_hdr, &response_buf[..resp_len])
3822
+ .map_err(|e| format!("send: {e}"))
3823
+ });
3824
+
3825
+ let mut client = increment_client(svc, batch_client_config());
3826
+ connect_ready(&mut client);
3827
+
3828
+ let err = client
3829
+ .call_increment_batch(&[10, 20])
3830
+ .expect_err("wrong batch item_count");
3831
+ assert_eq!(err, NipcError::BadItemCount);
3832
+
3833
+ client.close();
3834
+ server.wait();
3835
+ cleanup_all(svc);
3836
+}
3837
+
3838
+#[test]
3839
+fn test_call_increment_batch_rejects_malformed_response_envelope_unix() {
3840
+ struct Case {
3841
+ name: &'static str,
3842
+ kind: u16,
3843
+ code: u16,
3844
+ status: u16,
3845
+ message_id_delta: u64,
3846
+ want: NipcError,
3847
+ }
3848
+
3849
+ let cases = [
3850
+ Case {
3851
+ name: "bad kind",
3852
+ kind: KIND_REQUEST,
3853
+ code: METHOD_INCREMENT,
3854
+ status: STATUS_OK,
3855
+ message_id_delta: 0,
3856
+ want: NipcError::BadKind,
3857
+ },
3858
+ Case {
3859
+ name: "bad code",
3860
+ kind: KIND_RESPONSE,
3861
+ code: METHOD_STRING_REVERSE,
3862
+ status: STATUS_OK,
3863
+ message_id_delta: 0,
3864
+ want: NipcError::BadLayout,
3865
+ },
3866
+ Case {
3867
+ name: "bad status",
3868
+ kind: KIND_RESPONSE,
3869
+ code: METHOD_INCREMENT,
3870
+ status: STATUS_INTERNAL_ERROR,
3871
+ message_id_delta: 0,
3872
+ want: NipcError::BadLayout,
3873
+ },
3874
+ Case {
3875
+ name: "bad message id",
3876
+ kind: KIND_RESPONSE,
3877
+ code: METHOD_INCREMENT,
3878
+ status: STATUS_OK,
3879
+ message_id_delta: 1,
3880
+ want: NipcError::Truncated,
3881
+ },
3882
+ ];
3883
+
3884
+ for tc in cases {
3885
+ let svc = format!("rs_unix_batch_env_{}", tc.name.replace(' ', "_"));
3886
+ let mut server =
3887
+ start_raw_session_server(&svc, batch_server_config(), move |session, req_hdr, _| {
3888
+ let mut encoded = [0u8; INCREMENT_PAYLOAD_SIZE];
3889
+ let n = increment_encode(11, &mut encoded);
3890
+ if n != INCREMENT_PAYLOAD_SIZE {
3891
+ return Err(format!("increment_encode returned {n}"));
3892
+ }
3893
+
3894
+ let mut response_buf = vec![0u8; 128];
3895
+ let resp_len = {
3896
+ let mut batch = BatchBuilder::new(&mut response_buf, 2);
3897
+ batch
3898
+ .add(&encoded)
3899
+ .map_err(|e| format!("batch add 1: {e:?}"))?;
3900
+ batch
3901
+ .add(&encoded)
3902
+ .map_err(|e| format!("batch add 2: {e:?}"))?;
3903
+ let (len, _count) = batch.finish();
3904
+ len
3905
+ };
3906
+
3907
+ let mut resp_hdr = Header {
3908
+ kind: tc.kind,
3909
+ code: tc.code,
3910
+ flags: FLAG_BATCH,
3911
+ item_count: 2,
3912
+ message_id: req_hdr.message_id + tc.message_id_delta,
3913
+ transport_status: tc.status,
3914
+ ..Header::default()
3915
+ };
3916
+ session
3917
+ .send(&mut resp_hdr, &response_buf[..resp_len])
3918
+ .map_err(|e| format!("send: {e}"))
3919
+ });
3920
+
3921
+ let mut client = increment_client(&svc, batch_client_config());
3922
+ connect_ready(&mut client);
3923
+
3924
+ let err = client.call_increment_batch(&[10, 20]).expect_err(tc.name);
3925
+ assert_eq!(err, tc.want, "{}", tc.name);
3926
+
3927
+ client.close();
3928
+ server.wait();
3929
+ cleanup_all(&svc);
3930
+ }
3931
+}
3932
+
3933
+#[test]
3934
+fn test_call_string_reverse_chunked_response_unix() {
3935
+ let svc = "rs_unix_chunked_reverse";
3936
+ ensure_run_dir();
3937
+ cleanup_all(svc);
3938
+
3939
+ let long_input = "abcdefghi".repeat(16);
3940
+ let expected: String = long_input.chars().rev().collect();
3941
+ let scfg = ServerConfig {
3942
+ packet_size: 64,
3943
+ max_response_payload_bytes: 4096,
3944
+ ..server_config()
3945
+ };
3946
+ let ccfg = ClientConfig {
3947
+ packet_size: 64,
3948
+ max_response_payload_bytes: 4096,
3949
+ ..client_config()
3950
+ };
3951
+
3952
+ let mut server = TestServer::start_with(
3953
+ svc,
3954
+ scfg,
3955
+ METHOD_STRING_REVERSE,
3956
+ Some(string_reverse_dispatch_handler()),
3957
+ 8,
3958
+ );
3959
+ let mut client = string_reverse_client(svc, ccfg);
3960
+ connect_ready(&mut client);
3961
+
3962
+ let result = client
3963
+ .call_string_reverse(&long_input)
3964
+ .expect("chunked reverse");
3965
+ assert_eq!(result.as_str(), expected);
3966
+
3967
+ client.close();
3968
+ server.stop();
3969
+ cleanup_all(svc);
3970
+}
3971
+
3972
+// ---------------------------------------------------------------
3973
+// Batch dispatch: handler failure returns INTERNAL_ERROR (lines 1244-1248)
3974
+// ---------------------------------------------------------------
3975
+
3976
+#[test]
3977
+fn test_batch_dispatch_handler_failure() {
3978
+ let svc = "rs_svc_batchfail";
3979
+ ensure_run_dir();
3980
+ cleanup_all(svc);
3981
+
3982
+ // Handler that fails on the 2nd item
3983
+ fn fail_second_increment_handler() -> IncrementHandler {
3984
+ static CALL_COUNT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
3985
+ Arc::new(move |value| {
3986
+ let n = CALL_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3987
+ if n % 3 == 1 {
3988
+ return None;
3989
+ }
3990
+ Some(value + 1)
3991
+ })
3992
+ }
3993
+
3994
+ fn batch_server_config() -> ServerConfig {
3995
+ ServerConfig {
3996
+ supported_profiles: PROFILE_BASELINE,
3997
+ max_request_payload_bytes: 4096,
3998
+ max_request_batch_items: 16,
3999
+ max_response_payload_bytes: 4096,
4000
+ max_response_batch_items: 16,
4001
+ auth_token: AUTH_TOKEN,
4002
+ backlog: 4,
4003
+ ..ServerConfig::default()
4004
+ }
4005
+ }
4006
+
4007
+ fn batch_client_config() -> ClientConfig {
4008
+ ClientConfig {
4009
+ supported_profiles: PROFILE_BASELINE,
4010
+ max_request_payload_bytes: 4096,
4011
+ max_request_batch_items: 16,
4012
+ max_response_payload_bytes: 4096,
4013
+ max_response_batch_items: 16,
4014
+ auth_token: AUTH_TOKEN,
4015
+ ..ClientConfig::default()
4016
+ }
4017
+ }
4018
+
4019
+ let svc_name = svc.to_string();
4020
+ let ready_flag = Arc::new(AtomicBool::new(false));
4021
+ let ready_clone = ready_flag.clone();
4022
+
4023
+ let mut server_obj = ManagedServer::with_workers(
4024
+ TEST_RUN_DIR,
4025
+ &svc_name,
4026
+ batch_server_config(),
4027
+ METHOD_INCREMENT,
4028
+ Some(increment_dispatch(fail_second_increment_handler())),
4029
+ 8,
4030
+ );
4031
+ let stop_flag = server_obj.running_flag();
4032
+
4033
+ let thread_handle = thread::spawn(move || {
4034
+ ready_clone.store(true, Ordering::Release);
4035
+ let _ = server_obj.run();
4036
+ });
4037
+
4038
+ for _ in 0..2000 {
4039
+ if ready_flag.load(Ordering::Acquire) {
4040
+ break;
4041
+ }
4042
+ thread::sleep(Duration::from_micros(500));
4043
+ }
4044
+ thread::sleep(Duration::from_millis(50));
4045
+
4046
+ let mut client = increment_client(svc, batch_client_config());
4047
+ client.refresh();
4048
+ assert!(client.ready());
4049
+
4050
+ // Batch of 3: handler fails on the 2nd -> server returns INTERNAL_ERROR
4051
+ let values = vec![10u64, 20, 30];
4052
+ let result = client.call_increment_batch(&values);
4053
+ // The batch should fail because the handler returned None for item 2
4054
+ assert!(result.is_err());
4055
+
4056
+ client.close();
4057
+ stop_flag.store(false, Ordering::Release);
4058
+ let _ = thread_handle.join();
4059
+ cleanup_all(svc);
4060
+}
4061
+
4062
+#[test]
4063
+fn test_batch_dispatch_builder_overflow_retries_and_recovers() {
4064
+ let svc = "rs_svc_batch_overflow";
4065
+ ensure_run_dir();
4066
+ cleanup_all(svc);
4067
+
4068
+ let mut scfg = batch_server_config();
4069
+ scfg.max_response_payload_bytes = 8;
4070
+
4071
+ let mut server = TestServer::start_with(
4072
+ svc,
4073
+ scfg,
4074
+ METHOD_INCREMENT,
4075
+ Some(increment_dispatch_handler()),
4076
+ 8,
4077
+ );
4078
+ let mut client = increment_client(svc, batch_client_config());
4079
+ connect_ready(&mut client);
4080
+
4081
+ let values = client
4082
+ .call_increment_batch(&[10, 20])
4083
+ .expect("batch builder overflow should transparently reconnect and retry");
4084
+ assert_eq!(values, vec![11, 21]);
4085
+ assert!(
4086
+ client.ready(),
4087
+ "client should stay READY after overflow recovery"
4088
+ );
4089
+ assert!(
4090
+ client.status().reconnect_count >= 1,
4091
+ "overflow recovery should reconnect at least once"
4092
+ );
4093
+
4094
+ client.close();
4095
+ server.stop();
4096
+ cleanup_all(svc);
4097
+}
4098
+
4099
+#[cfg(target_os = "linux")]
4100
+#[test]
4101
+fn test_shm_batch_request_item_decode_failure_returns_bad_envelope() {
4102
+ let svc = "rs_svc_shm_batch_bad_item";
4103
+ ensure_run_dir();
4104
+ cleanup_all(svc);
4105
+
4106
+ let mut server = TestServer::start_with(
4107
+ svc,
4108
+ shm_server_config(),
4109
+ METHOD_INCREMENT,
4110
+ Some(increment_dispatch_handler()),
4111
+ 8,
4112
+ );
4113
+ let mut client = increment_client(svc, shm_client_config());
4114
+ connect_ready(&mut client);
4115
+ assert!(
4116
+ client.shm.is_some(),
4117
+ "expected SHM transport to be negotiated"
4118
+ );
4119
+
4120
+ let mut bad_payload = [0u8; 16];
4121
+ bad_payload[0..4].copy_from_slice(&0u32.to_ne_bytes());
4122
+ bad_payload[4..8].copy_from_slice(&32u32.to_ne_bytes());
4123
+ bad_payload[8..12].copy_from_slice(&0u32.to_ne_bytes());
4124
+ bad_payload[12..16].copy_from_slice(&4u32.to_ne_bytes());
4125
+
4126
+ let req_hdr = Header {
4127
+ magic: MAGIC_MSG,
4128
+ version: VERSION,
4129
+ header_len: protocol::HEADER_LEN,
4130
+ kind: KIND_REQUEST,
4131
+ code: METHOD_INCREMENT,
4132
+ flags: FLAG_BATCH,
4133
+ payload_len: bad_payload.len() as u32,
4134
+ item_count: 2,
4135
+ message_id: 7,
4136
+ transport_status: STATUS_OK,
4137
+ };
4138
+ let mut msg = [0u8; HEADER_SIZE + 16];
4139
+ req_hdr.encode(&mut msg[..HEADER_SIZE]);
4140
+ msg[HEADER_SIZE..].copy_from_slice(&bad_payload);
4141
+ client
4142
+ .shm
4143
+ .as_mut()
4144
+ .expect("shm")
4145
+ .send(&msg)
4146
+ .expect("send malformed batch request");
4147
+
4148
+ let (resp_hdr, response) = client.transport_receive().expect("receive response");
4149
+ assert_eq!(resp_hdr.kind, KIND_RESPONSE);
4150
+ assert_eq!(resp_hdr.code, METHOD_INCREMENT);
4151
+ assert_eq!(resp_hdr.transport_status, STATUS_BAD_ENVELOPE);
4152
+ assert_eq!(resp_hdr.flags, 0);
4153
+ assert_eq!(resp_hdr.item_count, 1);
4154
+ assert!(
4155
+ client
4156
+ .response_payload(response)
4157
+ .expect("response payload view")
4158
+ .is_empty(),
4159
+ "bad-envelope response should have no payload"
4160
+ );
4161
+
4162
+ client.close();
4163
+ server.stop();
4164
+ cleanup_all(svc);
4165
+}