//! Transport trait contract tests for `GrpcTransport`. //! //! Mirrors the test patterns from `InProcessTransport` but over gRPC on localhost. use std::collections::HashMap; use std::net::SocketAddr; use std::thread; use std::time::Duration; use tidaldb::replication::WalSegmentId; use tidaldb::replication::shard::{RegionId, ShardId}; use tidaldb::replication::transport::{Transport, TransportError, WalSegmentPayload}; use tidal_net::GrpcTransport; use tidal_net::config::GrpcTransportConfig; /// 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); } }