// Integration-test exemptions (same posture as the tidaldb integration tests): // unwrap/unwrap_err on known-good fixtures and short-lived read guards are // idiomatic here. #![allow(clippy::unwrap_used, clippy::significant_drop_tightening)] //! Transport trait contract tests for `GrpcTransport`. //! //! Mirrors the test patterns from `InProcessTransport` but over gRPC on localhost. use std::{collections::HashMap, net::SocketAddr, thread, time::Duration}; use tidal_net::{GrpcTransport, config::GrpcTransportConfig}; use tidaldb::replication::{ WalSegmentId, shard::{RegionId, ShardId}, transport::{Transport, TransportError, WalSegmentPayload}, }; /// Get a unique listen address using port 0 (OS-assigned). /// Since tonic doesn't support port 0, we bind a `TcpListener` to find a free port. fn free_addr() -> SocketAddr { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); listener.local_addr().unwrap() } fn make_config( shard: ShardId, listen: SocketAddr, peers: HashMap, ) -> GrpcTransportConfig { GrpcTransportConfig { local_shard: shard, listen_addr: listen, peers, insecure: true, ..Default::default() } } fn make_payload(shard: ShardId, seqno: u64) -> WalSegmentPayload { WalSegmentPayload { id: WalSegmentId::new(RegionId::SINGLE, shard, seqno), bytes: vec![0xAB; 100], event_count: 5, } } /// Build two `GrpcTransports` on localhost that can talk to each other. fn build_pair() -> (GrpcTransport, GrpcTransport) { let addr0 = free_addr(); let addr1 = free_addr(); let config0 = make_config(ShardId(0), addr0, HashMap::from([(ShardId(1), addr1)])); let config1 = make_config(ShardId(1), addr1, HashMap::from([(ShardId(0), addr0)])); let t0 = GrpcTransport::new(config0).expect("transport 0"); let t1 = GrpcTransport::new(config1).expect("transport 1"); // Give servers a moment to start. thread::sleep(Duration::from_millis(100)); (t0, t1) } #[test] fn send_and_receive_between_shards() { let (t0, t1) = build_pair(); // Shard 0 sends to Shard 1. let payload = make_payload(ShardId(0), 42); t0.send_segment(ShardId(1), payload).unwrap(); // Shard 1 receives it. let received = t1.recv_segment().unwrap(); assert_eq!(received.id.seqno, 42); assert_eq!(received.event_count, 5); assert_eq!(received.bytes.len(), 100); } #[test] fn send_to_unknown_peer_fails() { let addr0 = free_addr(); let config0 = make_config(ShardId(0), addr0, HashMap::new()); let t0 = GrpcTransport::new(config0).expect("transport 0"); thread::sleep(Duration::from_millis(50)); let result = t0.send_segment(ShardId(99), make_payload(ShardId(0), 1)); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), TransportError::UnknownPeer(_) )); } #[test] fn payload_too_large_rejected() { let addr0 = free_addr(); let addr1 = free_addr(); let config0 = make_config(ShardId(0), addr0, HashMap::from([(ShardId(1), addr1)])); let t0 = GrpcTransport::new(config0).expect("transport 0"); thread::sleep(Duration::from_millis(50)); let payload = WalSegmentPayload { id: WalSegmentId::new(RegionId::SINGLE, ShardId(0), 1), bytes: vec![0u8; 64 * 1024 * 1024 + 1], event_count: 0, }; let result = t0.send_segment(ShardId(1), payload); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), TransportError::PayloadTooLarge { .. } )); } #[test] fn local_shard_returns_correct_id() { let addr = free_addr(); let config = make_config(ShardId(7), addr, HashMap::new()); let t = GrpcTransport::new(config).expect("transport"); assert_eq!(t.local_shard(), ShardId(7)); } #[test] fn multiple_segments_fifo_order() { let (t0, t1) = build_pair(); for seq in 1..=5u64 { t0.send_segment(ShardId(1), make_payload(ShardId(0), seq)) .unwrap(); } // Brief pause to let segments flow through gRPC. thread::sleep(Duration::from_millis(50)); for expected_seq in 1..=5u64 { let received = t1.recv_segment().unwrap(); assert_eq!(received.id.seqno, expected_seq); } }