this repo has no description

Add comprehensive test suite for SWIM protocol

Test modules implemented:
- generators.ml: QCheck generators for all SWIM types (node_id, incarnation,
member_state, node_info, protocol_msg, packet, config, errors)
- test_codec.ml: Codec property tests (roundtrip, size accuracy) and unit tests
for encoder/decoder operations, error handling (19 tests)
- test_crypto.ml: Crypto property tests (roundtrip, overhead size) and unit tests
for key validation, tampering detection, nonce uniqueness (13 tests)
- test_pure.ml: Protocol_pure property tests (merge convergence, idempotence) and
unit tests for state transitions, invalidation, timeouts (32 tests)

All 65 tests passing.

+4 -4
.beads/issues.jsonl
··· 1 - {"id":"swim-294","title":"Implement test generators (test/generators.ml)","description":"Create QCheck generators for property-based testing.\n\n## Generators to implement\n\n### Basic types\n- `gen_node_id : node_id QCheck.Gen.t`\n- `gen_incarnation : incarnation QCheck.Gen.t`\n- `gen_member_state : member_state QCheck.Gen.t`\n\n### Node types\n- `gen_node_info : node_info QCheck.Gen.t`\n - Generate valid addresses\n - Random metadata strings\n\n### Protocol messages\n- `gen_ping : protocol_msg QCheck.Gen.t`\n- `gen_ping_req : protocol_msg QCheck.Gen.t`\n- `gen_ack : protocol_msg QCheck.Gen.t`\n- `gen_alive : protocol_msg QCheck.Gen.t`\n- `gen_suspect : protocol_msg QCheck.Gen.t`\n- `gen_dead : protocol_msg QCheck.Gen.t`\n- `gen_user_msg : protocol_msg QCheck.Gen.t`\n- `gen_protocol_msg : protocol_msg QCheck.Gen.t` (uniform choice)\n\n### Packets\n- `gen_packet : packet QCheck.Gen.t`\n - Valid cluster names\n - Primary + piggyback messages\n\n### Binary data\n- `gen_cstruct : Cstruct.t QCheck.Gen.t`\n - Various sizes\n\n### Arbitrary instances\n- `arb_*` wrappers with shrinkers where useful\n\n## Design constraints\n- Use QCheck.Gen combinators\n- Generate valid data by construction\n- Include edge cases (empty strings, max values)","acceptance_criteria":"- All message types have generators\n- Generators produce valid data\n- Good distribution of test cases","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-08T18:49:22.04090675+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:49:22.04090675+01:00","labels":["qcheck","test"],"dependencies":[{"issue_id":"swim-294","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:49:22.044472866+01:00","created_by":"gdiazlo"},{"issue_id":"swim-294","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:49:26.910584411+01:00","created_by":"gdiazlo"}]} 2 - {"id":"swim-461","title":"Implement crypto tests (test/test_crypto.ml)","description":"Property-based and unit tests for crypto module.\n\n## Property tests\n\n### Roundtrip\n- `test_crypto_roundtrip` - encrypt then decrypt equals original\n- Test with various data sizes\n\n### Key validation\n- `test_invalid_key_length_rejected`\n- Test 31, 32, 33 byte keys\n\n## Unit tests\n\n### Encryption\n- Test output size = input size + overhead (28 bytes)\n- Test nonce is prepended\n- Test different plaintexts produce different ciphertexts\n\n### Decryption\n- Test successful decryption\n- Test tampered ciphertext fails\n- Test truncated data fails\n- Test wrong key fails\n\n### Key initialization\n- Test valid 32-byte key\n- Test invalid lengths rejected\n\n## Security tests\n- Verify nonces are unique (probabilistic)\n- Verify ciphertext differs from plaintext\n\n## Design constraints\n- Use QCheck for property tests\n- Test all error paths\n- Don't expose key material in errors","acceptance_criteria":"- All property tests pass\n- All unit tests pass\n- Security properties verified\n- Error handling tested","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-08T18:49:51.401236876+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:49:51.401236876+01:00","labels":["crypto","security","test"],"dependencies":[{"issue_id":"swim-461","depends_on_id":"swim-hc9","type":"blocks","created_at":"2026-01-08T18:49:51.404483911+01:00","created_by":"gdiazlo"},{"issue_id":"swim-461","depends_on_id":"swim-294","type":"blocks","created_at":"2026-01-08T18:49:51.405793127+01:00","created_by":"gdiazlo"},{"issue_id":"swim-461","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:49:56.45969199+01:00","created_by":"gdiazlo"}]} 1 + {"id":"swim-294","title":"Implement test generators (test/generators.ml)","description":"Create QCheck generators for property-based testing.\n\n## Generators to implement\n\n### Basic types\n- `gen_node_id : node_id QCheck.Gen.t`\n- `gen_incarnation : incarnation QCheck.Gen.t`\n- `gen_member_state : member_state QCheck.Gen.t`\n\n### Node types\n- `gen_node_info : node_info QCheck.Gen.t`\n - Generate valid addresses\n - Random metadata strings\n\n### Protocol messages\n- `gen_ping : protocol_msg QCheck.Gen.t`\n- `gen_ping_req : protocol_msg QCheck.Gen.t`\n- `gen_ack : protocol_msg QCheck.Gen.t`\n- `gen_alive : protocol_msg QCheck.Gen.t`\n- `gen_suspect : protocol_msg QCheck.Gen.t`\n- `gen_dead : protocol_msg QCheck.Gen.t`\n- `gen_user_msg : protocol_msg QCheck.Gen.t`\n- `gen_protocol_msg : protocol_msg QCheck.Gen.t` (uniform choice)\n\n### Packets\n- `gen_packet : packet QCheck.Gen.t`\n - Valid cluster names\n - Primary + piggyback messages\n\n### Binary data\n- `gen_cstruct : Cstruct.t QCheck.Gen.t`\n - Various sizes\n\n### Arbitrary instances\n- `arb_*` wrappers with shrinkers where useful\n\n## Design constraints\n- Use QCheck.Gen combinators\n- Generate valid data by construction\n- Include edge cases (empty strings, max values)","acceptance_criteria":"- All message types have generators\n- Generators produce valid data\n- Good distribution of test cases","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-08T18:49:22.04090675+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T20:00:13.745057699+01:00","closed_at":"2026-01-08T20:00:13.745057699+01:00","close_reason":"Implemented all QCheck generators for SWIM types","labels":["qcheck","test"],"dependencies":[{"issue_id":"swim-294","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:49:22.044472866+01:00","created_by":"gdiazlo"},{"issue_id":"swim-294","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:49:26.910584411+01:00","created_by":"gdiazlo"}]} 2 + {"id":"swim-461","title":"Implement crypto tests (test/test_crypto.ml)","description":"Property-based and unit tests for crypto module.\n\n## Property tests\n\n### Roundtrip\n- `test_crypto_roundtrip` - encrypt then decrypt equals original\n- Test with various data sizes\n\n### Key validation\n- `test_invalid_key_length_rejected`\n- Test 31, 32, 33 byte keys\n\n## Unit tests\n\n### Encryption\n- Test output size = input size + overhead (28 bytes)\n- Test nonce is prepended\n- Test different plaintexts produce different ciphertexts\n\n### Decryption\n- Test successful decryption\n- Test tampered ciphertext fails\n- Test truncated data fails\n- Test wrong key fails\n\n### Key initialization\n- Test valid 32-byte key\n- Test invalid lengths rejected\n\n## Security tests\n- Verify nonces are unique (probabilistic)\n- Verify ciphertext differs from plaintext\n\n## Design constraints\n- Use QCheck for property tests\n- Test all error paths\n- Don't expose key material in errors","acceptance_criteria":"- All property tests pass\n- All unit tests pass\n- Security properties verified\n- Error handling tested","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-08T18:49:51.401236876+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T20:05:13.159541271+01:00","closed_at":"2026-01-08T20:05:13.159541271+01:00","close_reason":"Implemented crypto property and unit tests - all 13 tests passing","labels":["crypto","security","test"],"dependencies":[{"issue_id":"swim-461","depends_on_id":"swim-hc9","type":"blocks","created_at":"2026-01-08T18:49:51.404483911+01:00","created_by":"gdiazlo"},{"issue_id":"swim-461","depends_on_id":"swim-294","type":"blocks","created_at":"2026-01-08T18:49:51.405793127+01:00","created_by":"gdiazlo"},{"issue_id":"swim-461","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:49:56.45969199+01:00","created_by":"gdiazlo"}]} 3 3 {"id":"swim-90e","title":"Implement transport.ml - Eio UDP/TCP networking","description":"Implement network transport layer using Eio.\n\n## UDP Transport\n\n### Functions\n- `create_udp_socket : Eio.Net.t -\u003e addr:string -\u003e port:int -\u003e Eio.Net.datagram_socket`\n- `send_udp : Eio.Net.datagram_socket -\u003e Eio.Net.Sockaddr.datagram -\u003e Cstruct.t -\u003e unit`\n- `recv_udp : Eio.Net.datagram_socket -\u003e Cstruct.t -\u003e (int * Eio.Net.Sockaddr.datagram)`\n\n## TCP Transport (for large payloads)\n\n### Functions\n- `create_tcp_listener : Eio.Net.t -\u003e addr:string -\u003e port:int -\u003e Eio.Net.listening_socket`\n- `connect_tcp : Eio.Net.t -\u003e addr:Eio.Net.Sockaddr.stream -\u003e timeout:float -\u003e clock:Eio.Time.clock -\u003e (Eio.Net.stream_socket, send_error) result`\n- `send_tcp : Eio.Net.stream_socket -\u003e Cstruct.t -\u003e (unit, send_error) result`\n- `recv_tcp : Eio.Net.stream_socket -\u003e Cstruct.t -\u003e (int, [`Connection_reset]) result`\n\n## Address parsing\n- `parse_addr : string -\u003e (Eio.Net.Sockaddr.datagram, [`Invalid_addr]) result`\n - Parse \"host:port\" format\n\n## Design constraints\n- Use Eio.Net for all I/O\n- No blocking except Eio primitives\n- Proper error handling via Result\n- Support for IPv4 and IPv6","acceptance_criteria":"- UDP send/recv works\n- TCP connect/send/recv works\n- Proper error handling\n- Address parsing robust","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:48:09.296035344+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:39:34.082898832+01:00","closed_at":"2026-01-08T19:39:34.082898832+01:00","close_reason":"Implemented UDP and TCP transport with Eio.Net, plus address parsing (mli skipped due to complex Eio row types)","labels":["core","eio","transport"],"dependencies":[{"issue_id":"swim-90e","depends_on_id":"swim-oun","type":"blocks","created_at":"2026-01-08T18:48:09.299855321+01:00","created_by":"gdiazlo"},{"issue_id":"swim-90e","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:48:15.52111057+01:00","created_by":"gdiazlo"}]} 4 4 {"id":"swim-don","title":"Implement benchmarks (bench/)","description":"Performance benchmarks for critical paths.\n\n## bench/bench_codec.ml\n- `bench_encode_ping` - encoding a Ping message\n- `bench_encode_packet` - full packet with piggyback\n- `bench_decode_packet` - decoding a packet\n- `bench_encoded_size` - size calculation\n\n## bench/bench_crypto.ml\n- `bench_encrypt` - encryption throughput\n- `bench_decrypt` - decryption throughput\n- `bench_key_init` - key initialization\n\n## bench/bench_throughput.ml\n- `bench_broadcast_throughput` - messages/second\n- `bench_probe_cycle` - probe cycle latency\n- `bench_concurrent_probes` - parallel probe handling\n\n## bench/bench_allocations.ml\n- `bench_probe_cycle_allocations` - count allocations per probe\n- `bench_buffer_reuse_rate` - % of buffers reused\n- `bench_message_handling_allocations` - allocations per message\n\n## Performance targets to verify\n- \u003c 5 allocations per probe cycle\n- \u003e 95% buffer reuse rate\n- \u003c 3 seconds failure detection\n- \u003e 10,000 broadcast/sec\n- \u003c 1% CPU idle, \u003c 5% under load\n\n## Design constraints\n- Use core_bench or similar\n- Warm up before measuring\n- Multiple iterations for stability\n- Report with confidence intervals","acceptance_criteria":"- All benchmarks run\n- Performance targets documented\n- Regression detection possible\n- Results reproducible","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-08T18:50:57.818433013+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:50:57.818433013+01:00","labels":["bench","performance"],"dependencies":[{"issue_id":"swim-don","depends_on_id":"swim-zsi","type":"blocks","created_at":"2026-01-08T18:50:57.821397737+01:00","created_by":"gdiazlo"},{"issue_id":"swim-don","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:51:03.066326187+01:00","created_by":"gdiazlo"}]} 5 5 {"id":"swim-etm","title":"Implement pending_acks.ml - Ack tracking with promises","description":"Implement pending ack tracking for probe responses.\n\n## Pending_acks module\n```ocaml\ntype waiter = {\n promise : string option Eio.Promise.t;\n resolver : string option Eio.Promise.u;\n}\n\ntype t = {\n table : (int, waiter) Kcas_data.Hashtbl.t;\n}\n```\n\n### Functions\n- `create : unit -\u003e t`\n\n- `register : t -\u003e seq:int -\u003e waiter`\n - Create promise/resolver pair\n - Store in hashtable keyed by sequence number\n - Return waiter handle\n\n- `complete : t -\u003e seq:int -\u003e payload:string option -\u003e bool`\n - Find waiter by seq\n - Resolve promise with payload\n - Remove from table\n - Return true if found\n\n- `wait : waiter -\u003e timeout:float -\u003e clock:Eio.Time.clock -\u003e string option option`\n - Wait for promise with timeout\n - Return Some payload on success\n - Return None on timeout\n\n- `cancel : t -\u003e seq:int -\u003e unit`\n - Remove waiter from table\n - Called on timeout to cleanup\n\n## Design constraints\n- Use Eio.Promise for async waiting\n- Use Eio.Time.with_timeout for timeouts\n- Lock-free via Kcas_data.Hashtbl\n- Cleanup on timeout to prevent leaks","acceptance_criteria":"- Acks properly matched to probes\n- Timeouts work correctly\n- No memory leaks on timeout\n- Concurrent completion safe","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:47:51.390307674+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:35:56.984403853+01:00","closed_at":"2026-01-08T19:35:56.984403853+01:00","close_reason":"Implemented pending_acks with Eio.Promise for async waiting and Kcas_data.Hashtbl for lock-free storage","labels":["core","kcas","protocol"],"dependencies":[{"issue_id":"swim-etm","depends_on_id":"swim-oun","type":"blocks","created_at":"2026-01-08T18:47:51.394677184+01:00","created_by":"gdiazlo"},{"issue_id":"swim-etm","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:47:57.657173744+01:00","created_by":"gdiazlo"}]} 6 6 {"id":"swim-fac","title":"Implement protocol_pure.ml - Pure SWIM state transitions","description":"Implement pure (no effects) SWIM protocol logic for state transitions.\n\n## Core abstraction\n```ocaml\ntype 'a transition = {\n new_state : 'a;\n broadcasts : protocol_msg list;\n events : node_event list;\n}\n```\n\n## State transition functions\n- `handle_alive : member_state -\u003e alive_msg -\u003e now:float -\u003e member_state transition`\n- `handle_suspect : member_state -\u003e suspect_msg -\u003e now:float -\u003e member_state transition`\n- `handle_dead : member_state -\u003e dead_msg -\u003e now:float -\u003e member_state transition`\n- `handle_ack : probe_state -\u003e ack_msg -\u003e probe_state transition`\n\n## Timeout calculations\n- `suspicion_timeout : config -\u003e node_count:int -\u003e float`\n - Based on suspicion_mult and log(node_count)\n - Capped by suspicion_max_timeout\n\n## Probe target selection\n- `next_probe_target : probe_index:int -\u003e members:node list -\u003e (node * int) option`\n - Round-robin with wraparound\n - Skip self\n\n## Message invalidation (for queue pruning)\n- `invalidates : protocol_msg -\u003e protocol_msg -\u003e bool`\n - Alive invalidates Suspect for same node with \u003e= incarnation\n - Dead invalidates everything for same node\n - Suspect invalidates older Suspect\n\n## State merging\n- `merge_member_state : local:member_state -\u003e remote:member_state -\u003e member_state`\n - CRDT-style merge based on incarnation\n - Dead is final (tombstone)\n - Higher incarnation wins\n\n## Retransmit calculation\n- `retransmit_limit : config -\u003e node_count:int -\u003e int`\n - Based on retransmit_mult * ceil(log(node_count + 1))\n\n## Design constraints\n- PURE functions only - no I/O, no time, no randomness\n- All inputs explicit\n- Exhaustive pattern matching\n- Fully testable with property-based tests","acceptance_criteria":"- All functions are pure (no effects)\n- Property-based tests for SWIM invariants\n- Incarnation ordering correct\n- Suspicion timeout formula matches SWIM paper","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:46:48.400928801+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:29:29.816719466+01:00","closed_at":"2026-01-08T19:29:29.816719466+01:00","close_reason":"Implemented all pure SWIM state transitions: handle_alive, handle_suspect, handle_dead, suspicion_timeout, retransmit_limit, next_probe_target, invalidates, merge_member_state, select_indirect_targets","labels":["core","protocol","pure"],"dependencies":[{"issue_id":"swim-fac","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:46:48.40501031+01:00","created_by":"gdiazlo"},{"issue_id":"swim-fac","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:46:52.770706917+01:00","created_by":"gdiazlo"}]} 7 7 {"id":"swim-hc9","title":"Implement crypto.ml - AES-256-GCM encryption","description":"Implement encryption layer using mirage-crypto for AES-256-GCM.\n\n## Constants\n- `nonce_size = 12`\n- `tag_size = 16`\n- `overhead = nonce_size + tag_size` (28 bytes)\n\n## Functions\n\n### Key initialization\n- `init_key : string -\u003e (key, [`Invalid_key_length]) result`\n- Must be exactly 32 bytes for AES-256\n\n### Encryption\n- `encrypt : key -\u003e Cstruct.t -\u003e Cstruct.t`\n- Generate random nonce via mirage-crypto-rng\n- Prepend nonce to ciphertext\n- Result: nonce (12) + ciphertext + tag (16)\n\n### Decryption\n- `decrypt : key -\u003e Cstruct.t -\u003e (Cstruct.t, [`Too_short | `Decryption_failed]) result`\n- Extract nonce from first 12 bytes\n- Verify and decrypt remaining data\n- Return plaintext or error\n\n## Design constraints\n- Use mirage-crypto.Cipher_block.AES.GCM\n- Use mirage-crypto-rng for nonce generation\n- Return Result types, no exceptions\n- Consider in-place decryption where possible","acceptance_criteria":"- Property-based roundtrip tests pass\n- Invalid data properly rejected\n- Key validation works\n- Nonces are unique (use RNG)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:46:09.946405585+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:24:49.736202746+01:00","closed_at":"2026-01-08T19:24:49.736202746+01:00","close_reason":"Implemented crypto.ml with AES-256-GCM using mirage-crypto. Uses Eio.Flow for secure random nonce generation.","labels":["core","crypto","security"],"dependencies":[{"issue_id":"swim-hc9","depends_on_id":"swim-oun","type":"blocks","created_at":"2026-01-08T18:46:09.950083952+01:00","created_by":"gdiazlo"},{"issue_id":"swim-hc9","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:46:14.608204384+01:00","created_by":"gdiazlo"}]} 8 8 {"id":"swim-iwg","title":"Implement dissemination.ml - Broadcast queue with invalidation","description":"Implement the broadcast queue for SWIM protocol message dissemination.\n\n## Broadcast_queue module\n```ocaml\ntype item = {\n msg : protocol_msg;\n transmits : int Kcas.Loc.t;\n created : Mtime.span;\n}\n\ntype t = {\n queue : item Kcas_data.Queue.t;\n depth : int Kcas.Loc.t;\n}\n```\n\n### Functions\n- `create : unit -\u003e t`\n\n- `enqueue : t -\u003e protocol_msg -\u003e transmits:int -\u003e created:Mtime.span -\u003e unit`\n - Add message with initial transmit count\n - Increment depth\n\n- `drain : t -\u003e max_bytes:int -\u003e encode_size:(protocol_msg -\u003e int) -\u003e protocol_msg list`\n - Pop messages up to max_bytes\n - Decrement transmit count\n - Re-enqueue if transmits \u003e 0\n - Return list of messages to piggyback\n\n- `depth : t -\u003e int`\n\n- `invalidate : t -\u003e invalidates:(protocol_msg -\u003e protocol_msg -\u003e bool) -\u003e protocol_msg -\u003e unit`\n - Remove messages invalidated by newer message\n - Uses Protocol_pure.invalidates\n\n## Design constraints\n- Lock-free via Kcas_data.Queue\n- Transmit counting for reliable dissemination\n- Size-aware draining for UDP packet limits\n- Message invalidation to prune stale updates","acceptance_criteria":"- Messages properly disseminated\n- Transmit counts respected\n- Invalidation works correctly\n- No message loss during concurrent access","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:47:32.926237507+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:34:04.973053383+01:00","closed_at":"2026-01-08T19:34:04.973053383+01:00","close_reason":"Implemented broadcast queue with enqueue, drain (size-aware), and invalidate functions using Kcas_data.Queue","labels":["core","dissemination","kcas"],"dependencies":[{"issue_id":"swim-iwg","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:47:32.933998652+01:00","created_by":"gdiazlo"},{"issue_id":"swim-iwg","depends_on_id":"swim-fac","type":"blocks","created_at":"2026-01-08T18:47:32.93580631+01:00","created_by":"gdiazlo"},{"issue_id":"swim-iwg","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:47:40.222942145+01:00","created_by":"gdiazlo"}]} 9 - {"id":"swim-l32","title":"Implement codec tests (test/test_codec.ml)","description":"Property-based and unit tests for codec module.\n\n## Property tests\n\n### Roundtrip\n- `test_codec_roundtrip` - encode then decode equals original\n- `test_encoder_decoder_roundtrip` - for primitive types\n\n### Size calculation\n- `test_encoded_size_accurate` - encoded_size matches actual encoding\n\n### Error handling\n- `test_invalid_magic_rejected`\n- `test_unsupported_version_rejected`\n- `test_truncated_message_rejected`\n- `test_invalid_tag_rejected`\n\n## Unit tests\n\n### Encoder\n- Test write_byte, write_int16_be, etc.\n- Test write_string with various lengths\n- Test buffer overflow detection\n\n### Decoder\n- Test read operations\n- Test remaining/is_empty\n- Test boundary conditions\n\n### Message encoding\n- Test each message type individually\n- Test packet with piggyback messages\n- Test empty piggyback list\n\n## Design constraints\n- Use QCheck for property tests\n- Use Alcotest or similar for unit tests\n- Cover all message types\n- Test error paths","acceptance_criteria":"- All property tests pass\n- All unit tests pass\n- Edge cases covered\n- Error handling tested","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-08T18:49:38.017959466+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:49:38.017959466+01:00","labels":["codec","test"],"dependencies":[{"issue_id":"swim-l32","depends_on_id":"swim-l5y","type":"blocks","created_at":"2026-01-08T18:49:38.021527282+01:00","created_by":"gdiazlo"},{"issue_id":"swim-l32","depends_on_id":"swim-294","type":"blocks","created_at":"2026-01-08T18:49:38.02331756+01:00","created_by":"gdiazlo"},{"issue_id":"swim-l32","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:49:42.065502393+01:00","created_by":"gdiazlo"}]} 9 + {"id":"swim-l32","title":"Implement codec tests (test/test_codec.ml)","description":"Property-based and unit tests for codec module.\n\n## Property tests\n\n### Roundtrip\n- `test_codec_roundtrip` - encode then decode equals original\n- `test_encoder_decoder_roundtrip` - for primitive types\n\n### Size calculation\n- `test_encoded_size_accurate` - encoded_size matches actual encoding\n\n### Error handling\n- `test_invalid_magic_rejected`\n- `test_unsupported_version_rejected`\n- `test_truncated_message_rejected`\n- `test_invalid_tag_rejected`\n\n## Unit tests\n\n### Encoder\n- Test write_byte, write_int16_be, etc.\n- Test write_string with various lengths\n- Test buffer overflow detection\n\n### Decoder\n- Test read operations\n- Test remaining/is_empty\n- Test boundary conditions\n\n### Message encoding\n- Test each message type individually\n- Test packet with piggyback messages\n- Test empty piggyback list\n\n## Design constraints\n- Use QCheck for property tests\n- Use Alcotest or similar for unit tests\n- Cover all message types\n- Test error paths","acceptance_criteria":"- All property tests pass\n- All unit tests pass\n- Edge cases covered\n- Error handling tested","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-08T18:49:38.017959466+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T20:03:07.370600701+01:00","closed_at":"2026-01-08T20:03:07.370600701+01:00","close_reason":"Implemented codec property and unit tests - all 19 tests passing","labels":["codec","test"],"dependencies":[{"issue_id":"swim-l32","depends_on_id":"swim-l5y","type":"blocks","created_at":"2026-01-08T18:49:38.021527282+01:00","created_by":"gdiazlo"},{"issue_id":"swim-l32","depends_on_id":"swim-294","type":"blocks","created_at":"2026-01-08T18:49:38.02331756+01:00","created_by":"gdiazlo"},{"issue_id":"swim-l32","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:49:42.065502393+01:00","created_by":"gdiazlo"}]} 10 10 {"id":"swim-l5y","title":"Implement codec.ml - Zero-copy binary encoding/decoding","description":"Implement binary encoding/decoding with zero-copy semantics using Cstruct.\n\n## Components\n\n### Encoder module\n- `type t` with buf and mutable pos\n- `create : buf:Cstruct.t -\u003e t`\n- `write_byte`, `write_int16_be`, `write_int32_be`, `write_int64_be`\n- `write_string` (length-prefixed)\n- `write_bytes`\n- `to_cstruct` - returns view, no copy\n- `reset`, `remaining`\n\n### Decoder module\n- `type t` with buf and mutable pos\n- `create : Cstruct.t -\u003e t`\n- `read_byte`, `read_int16_be`, `read_int32_be`, `read_int64_be`\n- `read_string` - returns string (must copy for safety)\n- `read_bytes` - returns Cstruct view\n- `remaining`, `is_empty`\n\n### Codec module\n- Magic bytes: \"SWIM\"\n- Version: 1\n- Message tags: 0x01-0x07 for each message type\n- `encode_packet : packet -\u003e buf:Cstruct.t -\u003e (int, [`Buffer_too_small]) result`\n- `decode_packet : Cstruct.t -\u003e packet decode_result`\n- `encoded_size : protocol_msg -\u003e int` for queue draining\n\n### Helper encoders\n- `encode_node`, `encode_node_id`\n- `encode_option`\n- `decode_msg`\n\n## Design constraints\n- No allocations in hot path except unavoidable string creation\n- Return Result types, no exceptions\n- Use Cstruct sub-views where possible","acceptance_criteria":"- Property-based roundtrip tests pass\n- No unnecessary allocations\n- All message types encode/decode correctly\n- Error handling for truncated/invalid data","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:45:54.407900731+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:23:12.726852552+01:00","closed_at":"2026-01-08T19:23:12.726852552+01:00","close_reason":"Implemented codec.ml with Encoder/Decoder modules, zero-copy encoding/decoding for all protocol messages, IP address parsing, and encoded_size calculation","labels":["codec","core","zero-copy"],"dependencies":[{"issue_id":"swim-l5y","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:45:54.412742463+01:00","created_by":"gdiazlo"},{"issue_id":"swim-l5y","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:45:59.779010836+01:00","created_by":"gdiazlo"}]} 11 11 {"id":"swim-oll","title":"Implement membership.ml - Kcas-based member table","description":"Implement lock-free membership state management using kcas and kcas_data.\n\n## Member module\n```ocaml\ntype t = {\n node : node_info; (* Immutable *)\n state : member_state Kcas.Loc.t;\n incarnation : incarnation Kcas.Loc.t;\n state_change_time : Mtime.span Kcas.Loc.t;\n last_ack_time : Mtime.span Kcas.Loc.t;\n}\n```\n\n### Functions\n- `create : node_info -\u003e t`\n- `node : t -\u003e node_info` (pure accessor)\n- `get_state`, `get_incarnation`, `get_last_ack` (kcas reads)\n- `set_alive`, `set_suspect`, `set_dead` with `~xt:Kcas.Xt.t`\n- `record_ack : t -\u003e now:Mtime.span -\u003e xt:Kcas.Xt.t -\u003e unit`\n- `snapshot : t -\u003e xt:Kcas.Xt.t -\u003e member_snapshot`\n\n## Membership module\n```ocaml\ntype t = {\n table : (string, Member.t) Kcas_data.Hashtbl.t;\n count : int Kcas.Loc.t;\n}\n```\n\n### Functions\n- `create : unit -\u003e t`\n- `add : t -\u003e Member.t -\u003e unit`\n- `remove : t -\u003e node_id -\u003e unit` (returns bool for success)\n- `find : t -\u003e node_id -\u003e Member.t option`\n- `mem : t -\u003e node_id -\u003e bool`\n- `to_list : t -\u003e Member.t list` (snapshot)\n- `count : t -\u003e int`\n- `update_member : t -\u003e node_id -\u003e (Member.t -\u003e xt:Kcas.Xt.t -\u003e unit) -\u003e bool`\n\n## Design constraints\n- All state via kcas locations\n- Use Kcas_data.Hashtbl for lock-free hashtable\n- Transactional updates via Kcas.Xt.commit\n- No I/O inside transactions\n- Short transactions only","acceptance_criteria":"- Lock-free operations work correctly\n- Concurrent access safe\n- Atomic state transitions\n- Snapshot consistency","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:47:11.022624275+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:33:07.449792483+01:00","closed_at":"2026-01-08T19:33:07.449792483+01:00","close_reason":"Implemented Member module with kcas locations and Membership table with Kcas_data.Hashtbl","labels":["core","kcas","membership"],"dependencies":[{"issue_id":"swim-oll","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:47:11.047048045+01:00","created_by":"gdiazlo"},{"issue_id":"swim-oll","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:47:20.00544253+01:00","created_by":"gdiazlo"}]} 12 12 {"id":"swim-oun","title":"Project setup: dune-project, opam, dependencies","description":"Set up the project structure and dependencies for the SWIM library.\n\n## Tasks\n1. Update dune-project with proper metadata and dependencies\n2. Configure swim.opam with all required dependencies:\n - eio (\u003e= 1.0)\n - kcas (\u003e= 0.7)\n - kcas_data (\u003e= 0.7)\n - mirage-crypto\n - mirage-crypto-rng\n - cstruct\n - qcheck (for testing)\n3. Create lib/dune with proper library configuration\n4. Create test/dune for test configuration\n5. Create bench/dune for benchmarks (optional initially)\n6. Verify project builds with `dune build`","acceptance_criteria":"- dune build succeeds\n- opam install . --deps-only works\n- All dependencies available","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:45:16.711747605+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:13:17.972217465+01:00","closed_at":"2026-01-08T19:13:17.972217465+01:00","close_reason":"Project setup complete: dune-project, lib/dune, test/dune configured. Build and tests pass.","labels":["infrastructure","setup"],"dependencies":[{"issue_id":"swim-oun","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:45:20.330948173+01:00","created_by":"gdiazlo"}]} 13 13 {"id":"swim-szx","title":"Implement kcas data structure tests (test/test_kcas.ml)","description":"Concurrent correctness tests for kcas-based data structures.\n\n## Buffer_pool tests\n- `test_buffer_pool_no_leaks` - all acquired buffers released\n- `test_buffer_pool_concurrent` - multiple fibers acquiring/releasing\n- `test_with_buffer_exception_safe` - buffer released on exception\n\n## Membership tests\n- `test_membership_concurrent_add_remove` - no lost updates\n- `test_membership_snapshot_consistency` - to_list is consistent\n- `test_membership_count_accurate` - count matches actual\n\n## Broadcast_queue tests\n- `test_broadcast_queue_fifo` - messages dequeued in order\n- `test_broadcast_queue_transmit_counting` - transmits decremented correctly\n- `test_broadcast_queue_invalidation` - old messages pruned\n- `test_broadcast_queue_concurrent` - concurrent enqueue/drain safe\n\n## Pending_acks tests\n- `test_pending_acks_complete` - ack resolves waiter\n- `test_pending_acks_timeout` - timeout returns None\n- `test_pending_acks_cancel` - cancel removes waiter\n- `test_pending_acks_concurrent` - multiple pending acks\n\n## Transactional tests\n- `test_atomic_member_update` - multi-location update is atomic\n- `test_transaction_retry` - conflicting transactions retry\n\n## Design constraints\n- Use Eio for concurrency\n- Test with multiple domains if possible\n- Verify linearizability properties","acceptance_criteria":"- All concurrent tests pass\n- No race conditions\n- Atomicity verified\n- Stress tests pass","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-08T18:50:25.944980162+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:50:25.944980162+01:00","labels":["concurrency","kcas","test"],"dependencies":[{"issue_id":"swim-szx","depends_on_id":"swim-xoo","type":"blocks","created_at":"2026-01-08T18:50:25.94903667+01:00","created_by":"gdiazlo"},{"issue_id":"swim-szx","depends_on_id":"swim-oll","type":"blocks","created_at":"2026-01-08T18:50:25.950569487+01:00","created_by":"gdiazlo"},{"issue_id":"swim-szx","depends_on_id":"swim-iwg","type":"blocks","created_at":"2026-01-08T18:50:25.951465481+01:00","created_by":"gdiazlo"},{"issue_id":"swim-szx","depends_on_id":"swim-etm","type":"blocks","created_at":"2026-01-08T18:50:25.952262505+01:00","created_by":"gdiazlo"},{"issue_id":"swim-szx","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:50:30.713954321+01:00","created_by":"gdiazlo"}]} 14 14 {"id":"swim-t28","title":"Implement protocol.ml - Main protocol loop and handlers","description":"Implement the effectful protocol runner that applies pure transitions.\n\n## Main Cluster Type\n```ocaml\ntype t = {\n config : config;\n env : env;\n self : node;\n members : Membership.t;\n incarnation : int Kcas.Loc.t;\n sequence : int Kcas.Loc.t;\n broadcast_queue : Broadcast_queue.t;\n pending_acks : Pending_acks.t;\n probe_index : int Kcas.Loc.t;\n send_pool : Buffer_pool.t;\n recv_pool : Buffer_pool.t;\n udp_sock : Eio.Net.datagram_socket;\n tcp_listener : Eio.Net.listening_socket;\n event_stream : node_event Eio.Stream.t;\n handlers : (node -\u003e string -\u003e string -\u003e unit) list Kcas.Loc.t;\n cipher_key : Mirage_crypto.Cipher_block.AES.GCM.key;\n stats : stats Kcas.Loc.t;\n shutdown : bool Kcas.Loc.t;\n}\n```\n\n## Protocol Loop\n- `run_protocol : t -\u003e unit`\n - Main loop: probe cycle, timing, check shutdown\n - Use Protocol_pure for state transitions\n\n- `probe_cycle : t -\u003e Member.t -\u003e unit`\n - Get sequence number\n - Drain piggyback messages\n - Send ping\n - Wait for ack with timeout\n - On timeout: indirect probe\n\n- `indirect_probe : t -\u003e Member.t -\u003e seq:int -\u003e now:Mtime.span -\u003e unit`\n - Select k random members\n - Send ping_req through them\n - Wait for any ack\n\n## Receive Loop\n- `run_udp_receiver : t -\u003e unit`\n - Acquire buffer from pool\n - Receive packet\n - Fork fiber for processing\n - Release buffer after processing\n\n- `handle_udp_packet : t -\u003e buf:Cstruct.t -\u003e addr:Eio.Net.Sockaddr.datagram -\u003e unit`\n - Decrypt\n - Decode\n - Dispatch to handler\n\n## Message Handlers\n- `handle_packet : t -\u003e addr:Eio.Net.Sockaddr.datagram -\u003e packet -\u003e unit`\n- `handle_ping : t -\u003e Ping.t -\u003e unit`\n- `handle_ping_req : t -\u003e Ping_req.t -\u003e unit`\n- `handle_ack : t -\u003e Ack.t -\u003e unit`\n- `handle_alive : t -\u003e Alive.t -\u003e unit`\n- `handle_suspect : t -\u003e Suspect.t -\u003e unit`\n- `handle_dead : t -\u003e Dead.t -\u003e unit`\n- `handle_user_msg : t -\u003e User_msg.t -\u003e unit`\n\n## Design constraints\n- Thin effectful wrapper over Protocol_pure\n- Use kcas for all state\n- Buffer pool for zero-copy I/O\n- Fork fibers for concurrent handling","acceptance_criteria":"- Protocol loop runs correctly\n- Probe cycles at configured interval\n- All message types handled\n- Stats updated accurately","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:48:36.304687885+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:53:04.782054511+01:00","closed_at":"2026-01-08T19:53:04.782054511+01:00","close_reason":"Implemented main protocol loop with all message handlers, probe cycles, indirect probing, UDP receiver, and cluster state management","labels":["core","eio","protocol"],"dependencies":[{"issue_id":"swim-t28","depends_on_id":"swim-td8","type":"blocks","created_at":"2026-01-08T18:48:36.308642743+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-l5y","type":"blocks","created_at":"2026-01-08T18:48:36.310137809+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-hc9","type":"blocks","created_at":"2026-01-08T18:48:36.310988083+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-xoo","type":"blocks","created_at":"2026-01-08T18:48:36.311690387+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-fac","type":"blocks","created_at":"2026-01-08T18:48:36.3123488+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-oll","type":"blocks","created_at":"2026-01-08T18:48:36.313012122+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-iwg","type":"blocks","created_at":"2026-01-08T18:48:36.313695305+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-etm","type":"blocks","created_at":"2026-01-08T18:48:36.314462189+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-90e","type":"blocks","created_at":"2026-01-08T18:48:36.315296073+01:00","created_by":"gdiazlo"},{"issue_id":"swim-t28","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:48:48.416247923+01:00","created_by":"gdiazlo"}]} 15 15 {"id":"swim-td8","title":"Implement types.ml - Immutable message and node types","description":"Create the core immutable types for the SWIM protocol.\n\n## Types to implement\n\n### Node identification\n- `node_id = Node_id of string [@@unboxed]`\n- `incarnation = Incarnation of int [@@unboxed]`\n\n### Node information\n- `node_info` record with id, addr (Eio.Net.Sockaddr.datagram), meta\n\n### Member state\n- `member_state = Alive | Suspect | Dead`\n- `member_snapshot` record for pure operations\n\n### Protocol messages (pattern-matchable variants)\n- `Ping of { seq; sender }`\n- `Ping_req of { seq; target; sender }`\n- `Ack of { seq; responder; payload }`\n- `Alive of { node; incarnation }`\n- `Suspect of { node; incarnation; suspector }`\n- `Dead of { node; incarnation; declarator }`\n- `User_msg of { topic; payload; origin }`\n\n### Packet structure\n- `packet = { cluster; primary; piggyback }`\n\n### Error types\n- `decode_error` variants\n- `send_error` variants\n\n### Configuration\n- `config` record with all SWIM parameters\n- `default_config` value\n\n### Environment\n- `env` record with Eio dependencies (net, clock, mono_clock, random, sw)\n\n## Design constraints\n- All types immutable\n- Use [@@unboxed] where appropriate for performance\n- Pattern-matchable variants for protocol messages","acceptance_criteria":"- All types defined with proper signatures in types.mli\n- Types compile with dune build\n- No mutable fields except where kcas will manage them","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:45:34.790084068+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:16:46.941262108+01:00","closed_at":"2026-01-08T19:16:46.941262108+01:00","close_reason":"Implemented types.ml and types.mli with all core types: node_id, incarnation, node_info, member_state, protocol_msg, packet, decode_error, send_error, node_event, config, env, stats","labels":["core","types"],"dependencies":[{"issue_id":"swim-td8","depends_on_id":"swim-oun","type":"blocks","created_at":"2026-01-08T18:45:34.794012265+01:00","created_by":"gdiazlo"},{"issue_id":"swim-td8","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:45:39.489609655+01:00","created_by":"gdiazlo"}]} 16 - {"id":"swim-w4y","title":"Implement protocol_pure tests (test/test_pure.ml)","description":"Property-based tests for pure SWIM logic.\n\n## State transition properties\n\n### Incarnation ordering\n- `test_alive_dominates_suspect` - Alive with \u003e= incarnation beats Suspect\n- `test_higher_incarnation_wins` - Higher incarnation always dominates\n- `test_dead_is_final` - Dead state cannot be overridden\n\n### Message invalidation\n- `test_invalidation_transitive` - if A invalidates B and B invalidates C, A invalidates C\n- `test_alive_invalidates_suspect` - for same node with \u003e= incarnation\n- `test_dead_invalidates_all` - Dead invalidates Alive and Suspect for same node\n\n### Merge properties\n- `test_merge_commutative` - merge(a, b) = merge(b, a)\n- `test_merge_idempotent` - merge(a, a) = a\n- `test_merge_respects_incarnation` - higher incarnation wins\n\n### Timeout calculation\n- `test_suspicion_timeout_increases_with_nodes` - more nodes = longer timeout\n- `test_suspicion_timeout_bounded` - never exceeds max\n\n### Probe target selection\n- `test_probe_wraps_around` - index wraps at list end\n- `test_probe_skips_self` - self is never selected\n\n## Unit tests\n- Test specific transition scenarios\n- Test edge cases (empty member list, incarnation 0, etc.)\n\n## Design constraints\n- All tests on pure functions\n- No I/O or effects in tests\n- Comprehensive property coverage","acceptance_criteria":"- All SWIM invariants tested\n- Properties match SWIM paper\n- Edge cases covered\n- All tests pass","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-08T18:50:08.398465616+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:50:08.398465616+01:00","labels":["protocol","pure","test"],"dependencies":[{"issue_id":"swim-w4y","depends_on_id":"swim-fac","type":"blocks","created_at":"2026-01-08T18:50:08.402396924+01:00","created_by":"gdiazlo"},{"issue_id":"swim-w4y","depends_on_id":"swim-294","type":"blocks","created_at":"2026-01-08T18:50:08.40380169+01:00","created_by":"gdiazlo"},{"issue_id":"swim-w4y","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:50:13.114782761+01:00","created_by":"gdiazlo"}]} 16 + {"id":"swim-w4y","title":"Implement protocol_pure tests (test/test_pure.ml)","description":"Property-based tests for pure SWIM logic.\n\n## State transition properties\n\n### Incarnation ordering\n- `test_alive_dominates_suspect` - Alive with \u003e= incarnation beats Suspect\n- `test_higher_incarnation_wins` - Higher incarnation always dominates\n- `test_dead_is_final` - Dead state cannot be overridden\n\n### Message invalidation\n- `test_invalidation_transitive` - if A invalidates B and B invalidates C, A invalidates C\n- `test_alive_invalidates_suspect` - for same node with \u003e= incarnation\n- `test_dead_invalidates_all` - Dead invalidates Alive and Suspect for same node\n\n### Merge properties\n- `test_merge_commutative` - merge(a, b) = merge(b, a)\n- `test_merge_idempotent` - merge(a, a) = a\n- `test_merge_respects_incarnation` - higher incarnation wins\n\n### Timeout calculation\n- `test_suspicion_timeout_increases_with_nodes` - more nodes = longer timeout\n- `test_suspicion_timeout_bounded` - never exceeds max\n\n### Probe target selection\n- `test_probe_wraps_around` - index wraps at list end\n- `test_probe_skips_self` - self is never selected\n\n## Unit tests\n- Test specific transition scenarios\n- Test edge cases (empty member list, incarnation 0, etc.)\n\n## Design constraints\n- All tests on pure functions\n- No I/O or effects in tests\n- Comprehensive property coverage","acceptance_criteria":"- All SWIM invariants tested\n- Properties match SWIM paper\n- Edge cases covered\n- All tests pass","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-08T18:50:08.398465616+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T20:08:47.505087764+01:00","closed_at":"2026-01-08T20:08:47.505087764+01:00","close_reason":"Implemented protocol_pure property and unit tests - all 32 tests passing","labels":["protocol","pure","test"],"dependencies":[{"issue_id":"swim-w4y","depends_on_id":"swim-fac","type":"blocks","created_at":"2026-01-08T18:50:08.402396924+01:00","created_by":"gdiazlo"},{"issue_id":"swim-w4y","depends_on_id":"swim-294","type":"blocks","created_at":"2026-01-08T18:50:08.40380169+01:00","created_by":"gdiazlo"},{"issue_id":"swim-w4y","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:50:13.114782761+01:00","created_by":"gdiazlo"}]} 17 17 {"id":"swim-wdc","title":"SWIM Protocol Library Implementation","description":"Production-ready SWIM (Scalable Weakly-consistent Infection-style Process Group Membership) protocol library in OCaml 5 for cluster membership, failure detection, and lightweight pub/sub messaging.\n\n## Core Design Principles\n- Pure functions by default, separate pure logic from effectful operations\n- Immutable data structures, mutations only through kcas\n- Zero-copy buffer management with buffer pools\n- Lock-free coordination via kcas/kcas_data\n- AES-256-GCM encryption\n\n## Dependencies (allowed)\n- eio (\u003e= 1.0), kcas (\u003e= 0.7), kcas_data (\u003e= 0.7)\n- mirage-crypto, mirage-crypto-rng, cstruct\n\n## Target Scale\n- Up to 100 nodes\n- Sub-second failure detection\n- Optimized for datacenter/cloud environments","acceptance_criteria":"- All 11 core modules implemented\n- Property-based tests for pure functions\n- Integration tests passing\n- Build and opam package working\n- Performance targets met (\u003c 5 allocations/probe, \u003e 95% buffer reuse)","status":"open","priority":1,"issue_type":"epic","created_at":"2026-01-08T18:45:08.49485159+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:45:08.49485159+01:00","labels":["epic","ocaml5","swim"]} 18 18 {"id":"swim-wwr","title":"Implement integration tests (test/test_integration.ml)","description":"End-to-end integration tests for the SWIM library.\n\n## Two-node tests\n- `test_two_node_join` - node2 joins node1, both see each other\n- `test_two_node_leave` - graceful leave propagates\n- `test_two_node_broadcast` - broadcast message received\n- `test_two_node_direct_send` - direct TCP message delivered\n\n## Three-node tests\n- `test_gossip_propagation` - message reaches all nodes\n- `test_indirect_probe` - indirect probe detects alive node\n- `test_failure_detection` - dead node detected and removed\n\n## Failure scenarios\n- `test_network_partition` - nodes handle partition\n- `test_node_crash` - crashed node detected as dead\n- `test_rejoin_after_crash` - node can rejoin after restart\n\n## Metadata tests\n- `test_metadata_propagation` - metadata updates reach all nodes\n- `test_metadata_update` - updated metadata replaces old\n\n## Event stream tests\n- `test_join_event_fired` - Join event on new member\n- `test_leave_event_fired` - Leave event on departure\n- `test_suspect_event_fired` - Suspect event on probe timeout\n\n## Performance tests\n- `test_convergence_time` - cluster converges within expected time\n- `test_message_throughput` - broadcast rate meets target\n\n## Design constraints\n- Use Eio_main.run for all tests\n- Proper cleanup with shutdown\n- Realistic timing (but accelerated)\n- Isolated network per test","acceptance_criteria":"- All integration tests pass\n- Failure scenarios handled\n- Performance targets met\n- Clean teardown","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-08T18:50:43.333077327+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T18:50:43.333077327+01:00","labels":["integration","test"],"dependencies":[{"issue_id":"swim-wwr","depends_on_id":"swim-zsi","type":"blocks","created_at":"2026-01-08T18:50:43.337480017+01:00","created_by":"gdiazlo"},{"issue_id":"swim-wwr","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:50:46.783801496+01:00","created_by":"gdiazlo"}]} 19 19 {"id":"swim-xoo","title":"Implement buffer_pool.ml - Buffer management with kcas_data","description":"Implement buffer pool for zero-copy network I/O.\n\n## Buffer_pool module\n\n### Type\n```ocaml\ntype t = {\n buffers : Cstruct.t Kcas_data.Queue.t;\n size : int;\n total : int;\n semaphore : Eio.Semaphore.t;\n}\n```\n\n### Functions\n- `create : size:int -\u003e count:int -\u003e t`\n - Pre-allocate `count` buffers of `size` bytes\n - Use Kcas_data.Queue for lock-free storage\n - Eio.Semaphore for blocking acquire\n\n- `acquire : t -\u003e Cstruct.t`\n - Block on semaphore if no buffers\n - Pop from queue\n - Reset buffer (memset 0) before returning\n\n- `try_acquire : t -\u003e Cstruct.t option`\n - Non-blocking acquire\n - Return None if no buffers available\n\n- `release : t -\u003e Cstruct.t -\u003e unit`\n - Push buffer back to queue\n - Release semaphore\n\n- `with_buffer : t -\u003e (Cstruct.t -\u003e 'a) -\u003e 'a`\n - RAII-style acquire/release\n - Use Fun.protect for exception safety\n\n- `available : t -\u003e int` - current available count\n- `total : t -\u003e int` - total pool size\n\n## Design constraints\n- Lock-free queue via kcas_data\n- Semaphore for blocking (only blocking allowed per spec)\n- Clear buffer ownership semantics\n- No memory leaks on exceptions","acceptance_criteria":"- Buffers properly recycled\n- No leaks under concurrent use\n- with_buffer is exception-safe\n- Stats accurate","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T18:46:28.146790073+01:00","created_by":"gdiazlo","updated_at":"2026-01-08T19:27:29.348943322+01:00","closed_at":"2026-01-08T19:27:29.348943322+01:00","close_reason":"Implemented buffer_pool.ml with Kcas_data.Queue and Eio.Semaphore","labels":["buffer","core","zero-copy"],"dependencies":[{"issue_id":"swim-xoo","depends_on_id":"swim-oun","type":"blocks","created_at":"2026-01-08T18:46:28.151030562+01:00","created_by":"gdiazlo"},{"issue_id":"swim-xoo","depends_on_id":"swim-wdc","type":"parent-child","created_at":"2026-01-08T18:46:32.927877844+01:00","created_by":"gdiazlo"}]}
+43 -1
test/dune
··· 1 + (library 2 + (name generators) 3 + (libraries swim qcheck cstruct mtime ipaddr eio) 4 + (modules generators)) 5 + 1 6 (test 2 7 (name test_swim) 3 8 (libraries 4 9 swim 10 + generators 5 11 alcotest 6 12 qcheck 7 13 qcheck-alcotest 8 - eio_main)) 14 + eio_main) 15 + (modules test_swim)) 16 + 17 + (test 18 + (name test_codec) 19 + (libraries 20 + swim 21 + generators 22 + alcotest 23 + qcheck 24 + qcheck-alcotest 25 + cstruct) 26 + (modules test_codec)) 27 + 28 + (test 29 + (name test_crypto) 30 + (libraries 31 + swim 32 + generators 33 + alcotest 34 + qcheck 35 + qcheck-alcotest 36 + cstruct 37 + eio_main) 38 + (modules test_crypto)) 39 + 40 + (test 41 + (name test_pure) 42 + (libraries 43 + swim 44 + generators 45 + alcotest 46 + qcheck 47 + qcheck-alcotest 48 + mtime 49 + eio) 50 + (modules test_pure))
+329
test/generators.ml
··· 1 + open Swim.Types 2 + 3 + let gen_node_id : node_id QCheck.Gen.t = 4 + let open QCheck.Gen in 5 + let+ id = 6 + oneof_weighted 7 + [ 8 + (3, string_size ~gen:printable (int_range 1 64)); 9 + (1, return ""); 10 + (1, string_size ~gen:printable (return 255)); 11 + ] 12 + in 13 + node_id_of_string id 14 + 15 + let gen_incarnation : incarnation QCheck.Gen.t = 16 + let open QCheck.Gen in 17 + let+ i = 18 + oneof_weighted 19 + [ (5, int_range 0 1000); (2, int_range 0 max_int); (1, return 0) ] 20 + in 21 + incarnation_of_int i 22 + 23 + let gen_member_state : member_state QCheck.Gen.t = 24 + let open QCheck.Gen in 25 + let alive : member_state = Alive in 26 + let suspect : member_state = Suspect in 27 + let dead : member_state = Dead in 28 + oneof [ return alive; return suspect; return dead ] 29 + 30 + let gen_ipv4 : string QCheck.Gen.t = 31 + let open QCheck.Gen in 32 + let+ a = int_range 0 255 33 + and+ b = int_range 0 255 34 + and+ c = int_range 0 255 35 + and+ d = int_range 0 255 in 36 + Printf.sprintf "%d.%d.%d.%d" a b c d 37 + 38 + let gen_port : int QCheck.Gen.t = 39 + let open QCheck.Gen in 40 + oneof_weighted 41 + [ (3, int_range 1024 65535); (1, int_range 1 1023); (1, return 7946) ] 42 + 43 + let gen_addr : addr QCheck.Gen.t = 44 + let open QCheck.Gen in 45 + let+ ip = gen_ipv4 and+ port = gen_port in 46 + let ipaddr = 47 + match Ipaddr.V4.of_string ip with 48 + | Ok v4 -> v4 49 + | Error _ -> Ipaddr.V4.localhost 50 + in 51 + `Udp (Eio.Net.Ipaddr.of_raw (Ipaddr.V4.to_octets ipaddr), port) 52 + 53 + let gen_meta : string QCheck.Gen.t = 54 + let open QCheck.Gen in 55 + oneof_weighted 56 + [ 57 + (3, string_size ~gen:printable (int_range 0 256)); 58 + (1, return ""); 59 + (1, return (String.make 1024 'x')); 60 + ] 61 + 62 + let gen_node_info : node_info QCheck.Gen.t = 63 + let open QCheck.Gen in 64 + let+ id = gen_node_id and+ addr = gen_addr and+ meta = gen_meta in 65 + make_node_info ~id ~addr ~meta 66 + 67 + let gen_seq : int QCheck.Gen.t = 68 + let open QCheck.Gen in 69 + oneof_weighted 70 + [ (5, int_range 0 10000); (2, int_range 0 max_int); (1, return 0) ] 71 + 72 + let gen_ping : protocol_msg QCheck.Gen.t = 73 + let open QCheck.Gen in 74 + let+ seq = gen_seq and+ sender = gen_node_info in 75 + Ping { seq; sender } 76 + 77 + let gen_ping_req : protocol_msg QCheck.Gen.t = 78 + let open QCheck.Gen in 79 + let+ seq = gen_seq and+ target = gen_node_id and+ sender = gen_node_info in 80 + Ping_req { seq; target; sender } 81 + 82 + let gen_payload : string option QCheck.Gen.t = 83 + let open QCheck.Gen in 84 + oneof_weighted 85 + [ 86 + (2, return None); 87 + (3, map Option.some (string_size ~gen:printable (int_range 0 512))); 88 + ] 89 + 90 + let gen_ack : protocol_msg QCheck.Gen.t = 91 + let open QCheck.Gen in 92 + let+ seq = gen_seq 93 + and+ responder = gen_node_info 94 + and+ payload = gen_payload in 95 + Ack { seq; responder; payload } 96 + 97 + let gen_alive : protocol_msg QCheck.Gen.t = 98 + let open QCheck.Gen in 99 + let+ node = gen_node_info and+ incarnation = gen_incarnation in 100 + Alive { node; incarnation } 101 + 102 + let gen_suspect : protocol_msg QCheck.Gen.t = 103 + let open QCheck.Gen in 104 + let+ node = gen_node_id 105 + and+ incarnation = gen_incarnation 106 + and+ suspector = gen_node_id in 107 + Suspect { node; incarnation; suspector } 108 + 109 + let gen_dead : protocol_msg QCheck.Gen.t = 110 + let open QCheck.Gen in 111 + let+ node = gen_node_id 112 + and+ incarnation = gen_incarnation 113 + and+ declarator = gen_node_id in 114 + Dead { node; incarnation; declarator } 115 + 116 + let gen_topic : string QCheck.Gen.t = 117 + QCheck.Gen.string_size ~gen:QCheck.Gen.printable (QCheck.Gen.int_range 1 64) 118 + 119 + let gen_user_payload : string QCheck.Gen.t = 120 + QCheck.Gen.string_size ~gen:QCheck.Gen.printable (QCheck.Gen.int_range 0 1024) 121 + 122 + let gen_user_msg : protocol_msg QCheck.Gen.t = 123 + let open QCheck.Gen in 124 + let+ topic = gen_topic 125 + and+ payload = gen_user_payload 126 + and+ origin = gen_node_id in 127 + User_msg { topic; payload; origin } 128 + 129 + let gen_protocol_msg : protocol_msg QCheck.Gen.t = 130 + QCheck.Gen.oneof 131 + [ 132 + gen_ping; 133 + gen_ping_req; 134 + gen_ack; 135 + gen_alive; 136 + gen_suspect; 137 + gen_dead; 138 + gen_user_msg; 139 + ] 140 + 141 + let gen_cluster_name : string QCheck.Gen.t = 142 + let open QCheck.Gen in 143 + oneof_weighted 144 + [ 145 + (3, string_size ~gen:printable (int_range 1 32)); 146 + (1, return "default"); 147 + (1, return "test-cluster"); 148 + ] 149 + 150 + let gen_piggyback : protocol_msg list QCheck.Gen.t = 151 + let open QCheck.Gen in 152 + let piggyback_msg = 153 + oneof [ gen_alive; gen_suspect; gen_dead; gen_user_msg ] 154 + in 155 + list_size (int_range 0 8) piggyback_msg 156 + 157 + let gen_packet : packet QCheck.Gen.t = 158 + let open QCheck.Gen in 159 + let+ cluster = gen_cluster_name 160 + and+ primary = gen_protocol_msg 161 + and+ piggyback = gen_piggyback in 162 + { cluster; primary; piggyback } 163 + 164 + let gen_cstruct : Cstruct.t QCheck.Gen.t = 165 + let open QCheck.Gen in 166 + let+ len = 167 + oneof_weighted 168 + [ (3, int_range 0 1024); (1, return 0); (1, int_range 1024 4096) ] 169 + and+ fill = char in 170 + let cs = Cstruct.create len in 171 + Cstruct.memset cs (Char.code fill); 172 + cs 173 + 174 + let gen_cstruct_sized (size : int) : Cstruct.t QCheck.Gen.t = 175 + let open QCheck.Gen in 176 + let+ bytes = string_size ~gen:char (return size) in 177 + Cstruct.of_string bytes 178 + 179 + let gen_config : config QCheck.Gen.t = 180 + let open QCheck.Gen in 181 + let+ bind_addr = gen_ipv4 182 + and+ bind_port = gen_port 183 + and+ node_name = 184 + oneof_weighted [ (2, return None); (3, map Option.some gen_topic) ] 185 + and+ protocol_interval = float_range 0.1 10.0 186 + and+ probe_timeout = float_range 0.1 5.0 187 + and+ indirect_checks = int_range 1 10 188 + and+ suspicion_mult = int_range 1 10 189 + and+ suspicion_max_timeout = float_range 10.0 120.0 190 + and+ retransmit_mult = int_range 1 10 191 + and+ udp_buffer_size = 192 + oneof [ return 1400; return 1500; return 8192; return 65507 ] 193 + and+ tcp_timeout = float_range 1.0 30.0 194 + and+ send_buffer_count = int_range 4 64 195 + and+ recv_buffer_count = int_range 4 64 196 + and+ secret_key = gen_cstruct_sized 32 197 + and+ cluster_name = gen_cluster_name in 198 + { 199 + bind_addr; 200 + bind_port; 201 + node_name; 202 + protocol_interval; 203 + probe_timeout; 204 + indirect_checks; 205 + suspicion_mult; 206 + suspicion_max_timeout; 207 + retransmit_mult; 208 + udp_buffer_size; 209 + tcp_timeout; 210 + send_buffer_count; 211 + recv_buffer_count; 212 + secret_key = Cstruct.to_string secret_key; 213 + cluster_name; 214 + } 215 + 216 + let gen_decode_error : decode_error QCheck.Gen.t = 217 + let open QCheck.Gen in 218 + oneof 219 + [ 220 + return Invalid_magic; 221 + map (fun v -> Unsupported_version v) (int_range 0 255); 222 + return Truncated_message; 223 + map (fun t -> Invalid_tag t) (int_range 0 255); 224 + return Decryption_failed; 225 + ] 226 + 227 + let gen_send_error : send_error QCheck.Gen.t = 228 + let open QCheck.Gen in 229 + oneof [ return Node_unreachable; return Timeout; return Connection_reset ] 230 + 231 + let gen_mtime_span : Mtime.span QCheck.Gen.t = 232 + let open QCheck.Gen in 233 + let+ ns = map Int64.of_int (int_range 0 1_000_000_000) in 234 + Mtime.Span.of_uint64_ns ns 235 + 236 + let gen_member_snapshot : member_snapshot QCheck.Gen.t = 237 + let open QCheck.Gen in 238 + let+ node = gen_node_info 239 + and+ state = gen_member_state 240 + and+ incarnation = gen_incarnation 241 + and+ state_change = gen_mtime_span in 242 + { node; state; incarnation; state_change } 243 + 244 + let arb_node_id : node_id QCheck.arbitrary = 245 + QCheck.make ~print:(fun id -> node_id_to_string id) gen_node_id 246 + 247 + let arb_incarnation : incarnation QCheck.arbitrary = 248 + QCheck.make 249 + ~print:(fun inc -> string_of_int (incarnation_to_int inc)) 250 + ~shrink:(fun inc -> 251 + let i = incarnation_to_int inc in 252 + QCheck.Shrink.int i |> QCheck.Iter.map incarnation_of_int) 253 + gen_incarnation 254 + 255 + let arb_member_state : member_state QCheck.arbitrary = 256 + QCheck.make ~print:member_state_to_string gen_member_state 257 + 258 + let format_addr (addr : addr) : string = 259 + match addr with 260 + | `Udp (ip, port) -> Fmt.str "%a:%d" Eio.Net.Ipaddr.pp ip port 261 + | `Unix path -> Printf.sprintf "unix:%s" path 262 + 263 + let format_node_info (ni : node_info) : string = 264 + Printf.sprintf "{ id=%s; addr=%s; meta=%S }" (node_id_to_string ni.id) 265 + (format_addr ni.addr) ni.meta 266 + 267 + let arb_node_info : node_info QCheck.arbitrary = 268 + QCheck.make ~print:format_node_info gen_node_info 269 + 270 + let format_protocol_msg (msg : protocol_msg) : string = 271 + match msg with 272 + | Ping { seq; sender } -> 273 + Printf.sprintf "Ping { seq=%d; sender=%s }" seq (format_node_info sender) 274 + | Ping_req { seq; target; sender } -> 275 + Printf.sprintf "Ping_req { seq=%d; target=%s; sender=%s }" seq 276 + (node_id_to_string target) (format_node_info sender) 277 + | Ack { seq; responder; payload } -> 278 + Printf.sprintf "Ack { seq=%d; responder=%s; payload=%s }" seq 279 + (format_node_info responder) 280 + (match payload with 281 + | None -> "None" 282 + | Some p -> Printf.sprintf "Some %S" p) 283 + | Alive { node; incarnation } -> 284 + Printf.sprintf "Alive { node=%s; incarnation=%d }" (format_node_info node) 285 + (incarnation_to_int incarnation) 286 + | Suspect { node; incarnation; suspector } -> 287 + Printf.sprintf "Suspect { node=%s; incarnation=%d; suspector=%s }" 288 + (node_id_to_string node) 289 + (incarnation_to_int incarnation) 290 + (node_id_to_string suspector) 291 + | Dead { node; incarnation; declarator } -> 292 + Printf.sprintf "Dead { node=%s; incarnation=%d; declarator=%s }" 293 + (node_id_to_string node) 294 + (incarnation_to_int incarnation) 295 + (node_id_to_string declarator) 296 + | User_msg { topic; payload; origin } -> 297 + Printf.sprintf "User_msg { topic=%S; payload=%S; origin=%s }" topic 298 + payload (node_id_to_string origin) 299 + 300 + let arb_protocol_msg : protocol_msg QCheck.arbitrary = 301 + QCheck.make ~print:format_protocol_msg gen_protocol_msg 302 + 303 + let format_packet (p : packet) : string = 304 + Printf.sprintf "{ cluster=%S; primary=%s; piggyback=[%d msgs] }" p.cluster 305 + (format_protocol_msg p.primary) 306 + (List.length p.piggyback) 307 + 308 + let arb_packet : packet QCheck.arbitrary = 309 + QCheck.make ~print:format_packet gen_packet 310 + 311 + let arb_cstruct : Cstruct.t QCheck.arbitrary = 312 + QCheck.make 313 + ~print:(fun cs -> Printf.sprintf "<cstruct len=%d>" (Cstruct.length cs)) 314 + gen_cstruct 315 + 316 + let arb_decode_error : decode_error QCheck.arbitrary = 317 + QCheck.make ~print:decode_error_to_string gen_decode_error 318 + 319 + let arb_send_error : send_error QCheck.arbitrary = 320 + QCheck.make ~print:send_error_to_string gen_send_error 321 + 322 + let arb_member_snapshot : member_snapshot QCheck.arbitrary = 323 + QCheck.make 324 + ~print:(fun ms -> 325 + Printf.sprintf "{ node=%s; state=%s; incarnation=%d }" 326 + (format_node_info ms.node) 327 + (member_state_to_string ms.state) 328 + (incarnation_to_int ms.incarnation)) 329 + gen_member_snapshot
+300
test/test_codec.ml
··· 1 + open Swim.Types 2 + open Swim.Codec 3 + 4 + let clamp_int32 n = n land 0x7FFFFFFF 5 + 6 + let clamp_incarnation inc = 7 + incarnation_of_int (clamp_int32 (incarnation_to_int inc)) 8 + 9 + let normalize_msg msg = 10 + match msg with 11 + | Ping { seq; sender } -> Ping { seq = clamp_int32 seq; sender } 12 + | Ping_req { seq; target; sender } -> 13 + Ping_req { seq = clamp_int32 seq; target; sender } 14 + | Ack { seq; responder; payload } -> 15 + Ack { seq = clamp_int32 seq; responder; payload } 16 + | Alive { node; incarnation } -> 17 + Alive { node; incarnation = clamp_incarnation incarnation } 18 + | Suspect { node; incarnation; suspector } -> 19 + Suspect { node; incarnation = clamp_incarnation incarnation; suspector } 20 + | Dead { node; incarnation; declarator } -> 21 + Dead { node; incarnation = clamp_incarnation incarnation; declarator } 22 + | User_msg _ as msg -> msg 23 + 24 + let normalize_packet packet = 25 + let primary = normalize_msg packet.primary in 26 + let piggyback = List.map normalize_msg packet.piggyback in 27 + { packet with primary; piggyback } 28 + 29 + let test_roundtrip_msg = 30 + QCheck.Test.make ~count:1000 ~name:"codec message roundtrip" 31 + Generators.arb_protocol_msg (fun msg -> 32 + let msg = normalize_msg msg in 33 + let size = encoded_size msg + 100 in 34 + let buf = Cstruct.create size in 35 + let enc = Encoder.create ~buf in 36 + encode_msg enc msg; 37 + let encoded = Encoder.to_cstruct enc in 38 + let dec = Decoder.create encoded in 39 + match decode_msg dec with Ok decoded -> decoded = msg | Error _ -> false) 40 + 41 + let test_roundtrip_packet = 42 + QCheck.Test.make ~count:500 ~name:"codec packet roundtrip" 43 + Generators.arb_packet (fun packet -> 44 + let packet = normalize_packet packet in 45 + let size = 46 + 4 + 1 + 2 47 + + String.length packet.cluster 48 + + 2 49 + + encoded_size packet.primary 50 + + List.fold_left (fun acc m -> acc + encoded_size m) 0 packet.piggyback 51 + + 100 52 + in 53 + let buf = Cstruct.create size in 54 + match encode_packet packet ~buf with 55 + | Error _ -> false 56 + | Ok len -> ( 57 + let encoded = Cstruct.sub buf 0 len in 58 + match decode_packet encoded with 59 + | Ok decoded -> decoded = packet 60 + | Error _ -> false)) 61 + 62 + let test_encoded_size_accurate = 63 + QCheck.Test.make ~count:1000 ~name:"encoded_size matches actual encoding" 64 + Generators.arb_protocol_msg (fun msg -> 65 + let predicted = encoded_size msg in 66 + let buf = Cstruct.create (predicted + 100) in 67 + let enc = Encoder.create ~buf in 68 + encode_msg enc msg; 69 + let actual = Encoder.pos enc in 70 + predicted = actual) 71 + 72 + let make_valid_packet_buf () = 73 + let node = 74 + make_node_info ~id:(node_id_of_string "n1") 75 + ~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7946)) 76 + ~meta:"" 77 + in 78 + let packet = 79 + { 80 + cluster = "test"; 81 + primary = Ping { seq = 1; sender = node }; 82 + piggyback = []; 83 + } 84 + in 85 + let buf = Cstruct.create 1000 in 86 + match encode_packet packet ~buf with 87 + | Ok len -> Cstruct.sub buf 0 len 88 + | Error _ -> failwith "encode failed" 89 + 90 + let test_invalid_magic_rejected () = 91 + let buf = make_valid_packet_buf () in 92 + Cstruct.blit_from_string "FAIL" 0 buf 0 4; 93 + match decode_packet buf with 94 + | Error Invalid_magic -> () 95 + | _ -> Alcotest.fail "expected Invalid_magic error" 96 + 97 + let test_unsupported_version_rejected () = 98 + let buf = make_valid_packet_buf () in 99 + Cstruct.set_uint8 buf 4 0x99; 100 + match decode_packet buf with 101 + | Error (Unsupported_version 0x99) -> () 102 + | Error (Unsupported_version v) -> 103 + Alcotest.failf "expected version 0x99 but got %d" v 104 + | _ -> Alcotest.fail "expected Unsupported_version error" 105 + 106 + let test_invalid_tag_rejected () = 107 + let buf = Cstruct.create 100 in 108 + let enc = Encoder.create ~buf in 109 + Encoder.write_bytes enc (Cstruct.of_string "SWIM"); 110 + Encoder.write_byte enc 1; 111 + Encoder.write_string enc "default"; 112 + Encoder.write_int16_be enc 1; 113 + Encoder.write_byte enc 0xFF; 114 + let encoded = Encoder.to_cstruct enc in 115 + match decode_packet encoded with 116 + | Error (Invalid_tag 0xFF) -> () 117 + | Error (Invalid_tag t) -> Alcotest.failf "expected tag 0xFF but got %d" t 118 + | _ -> Alcotest.fail "expected Invalid_tag error" 119 + 120 + let test_encoder_write_byte () = 121 + let buf = Cstruct.create 10 in 122 + let enc = Encoder.create ~buf in 123 + Encoder.write_byte enc 0x42; 124 + Encoder.write_byte enc 0xFF; 125 + let result = Encoder.to_cstruct enc in 126 + Alcotest.(check int) "length" 2 (Cstruct.length result); 127 + Alcotest.(check int) "byte 0" 0x42 (Cstruct.get_uint8 result 0); 128 + Alcotest.(check int) "byte 1" 0xFF (Cstruct.get_uint8 result 1) 129 + 130 + let test_encoder_write_int16_be () = 131 + let buf = Cstruct.create 10 in 132 + let enc = Encoder.create ~buf in 133 + Encoder.write_int16_be enc 0x1234; 134 + let result = Encoder.to_cstruct enc in 135 + Alcotest.(check int) "length" 2 (Cstruct.length result); 136 + Alcotest.(check int) "value" 0x1234 (Cstruct.BE.get_uint16 result 0) 137 + 138 + let test_encoder_write_int32_be () = 139 + let buf = Cstruct.create 10 in 140 + let enc = Encoder.create ~buf in 141 + Encoder.write_int32_be enc 0x12345678l; 142 + let result = Encoder.to_cstruct enc in 143 + Alcotest.(check int) "length" 4 (Cstruct.length result); 144 + Alcotest.(check int32) "value" 0x12345678l (Cstruct.BE.get_uint32 result 0) 145 + 146 + let test_encoder_write_string () = 147 + let buf = Cstruct.create 100 in 148 + let enc = Encoder.create ~buf in 149 + Encoder.write_string enc "hello"; 150 + let result = Encoder.to_cstruct enc in 151 + Alcotest.(check int) "length" 7 (Cstruct.length result); 152 + Alcotest.(check int) "str_len" 5 (Cstruct.BE.get_uint16 result 0); 153 + Alcotest.(check string) 154 + "content" "hello" 155 + (Cstruct.to_string ~off:2 ~len:5 result) 156 + 157 + let test_encoder_write_empty_string () = 158 + let buf = Cstruct.create 10 in 159 + let enc = Encoder.create ~buf in 160 + Encoder.write_string enc ""; 161 + let result = Encoder.to_cstruct enc in 162 + Alcotest.(check int) "length" 2 (Cstruct.length result); 163 + Alcotest.(check int) "str_len" 0 (Cstruct.BE.get_uint16 result 0) 164 + 165 + let test_decoder_read_byte () = 166 + let buf = Cstruct.of_string "\x42\xFF" in 167 + let dec = Decoder.create buf in 168 + Alcotest.(check int) "byte 0" 0x42 (Decoder.read_byte dec); 169 + Alcotest.(check int) "byte 1" 0xFF (Decoder.read_byte dec) 170 + 171 + let test_decoder_read_int16_be () = 172 + let buf = Cstruct.create 2 in 173 + Cstruct.BE.set_uint16 buf 0 0x1234; 174 + let dec = Decoder.create buf in 175 + Alcotest.(check int) "value" 0x1234 (Decoder.read_int16_be dec) 176 + 177 + let test_decoder_read_int32_be () = 178 + let buf = Cstruct.create 4 in 179 + Cstruct.BE.set_uint32 buf 0 0x12345678l; 180 + let dec = Decoder.create buf in 181 + Alcotest.(check int32) "value" 0x12345678l (Decoder.read_int32_be dec) 182 + 183 + let test_decoder_read_string () = 184 + let buf = Cstruct.create 10 in 185 + Cstruct.BE.set_uint16 buf 0 5; 186 + Cstruct.blit_from_string "hello" 0 buf 2 5; 187 + let dec = Decoder.create buf in 188 + Alcotest.(check string) "value" "hello" (Decoder.read_string dec) 189 + 190 + let test_decoder_remaining () = 191 + let buf = Cstruct.create 10 in 192 + let dec = Decoder.create buf in 193 + Alcotest.(check int) "initial" 10 (Decoder.remaining dec); 194 + let _ = Decoder.read_byte dec in 195 + Alcotest.(check int) "after byte" 9 (Decoder.remaining dec); 196 + let _ = Decoder.read_int32_be dec in 197 + Alcotest.(check int) "after int32" 5 (Decoder.remaining dec) 198 + 199 + let test_decoder_is_empty () = 200 + let buf = Cstruct.create 1 in 201 + let dec = Decoder.create buf in 202 + Alcotest.(check bool) "not empty" false (Decoder.is_empty dec); 203 + let _ = Decoder.read_byte dec in 204 + Alcotest.(check bool) "empty" true (Decoder.is_empty dec) 205 + 206 + let test_empty_piggyback () = 207 + let node = 208 + make_node_info 209 + ~id:(node_id_of_string "node1") 210 + ~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7946)) 211 + ~meta:"" 212 + in 213 + let packet = 214 + { 215 + cluster = "test"; 216 + primary = Ping { seq = 1; sender = node }; 217 + piggyback = []; 218 + } 219 + in 220 + let buf = Cstruct.create 1000 in 221 + match encode_packet packet ~buf with 222 + | Error _ -> Alcotest.fail "encode failed" 223 + | Ok len -> ( 224 + let encoded = Cstruct.sub buf 0 len in 225 + match decode_packet encoded with 226 + | Ok decoded -> 227 + Alcotest.(check int) 228 + "piggyback count" 0 229 + (List.length decoded.piggyback) 230 + | Error e -> Alcotest.failf "decode failed: %s" (decode_error_to_string e) 231 + ) 232 + 233 + let test_multiple_piggyback () = 234 + let node = 235 + make_node_info 236 + ~id:(node_id_of_string "node1") 237 + ~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7946)) 238 + ~meta:"" 239 + in 240 + let alive_state : member_state = Alive in 241 + let _ = alive_state in 242 + let piggyback = 243 + [ 244 + Alive { node; incarnation = incarnation_of_int 1 }; 245 + Suspect 246 + { 247 + node = node_id_of_string "node2"; 248 + incarnation = incarnation_of_int 2; 249 + suspector = node_id_of_string "node1"; 250 + }; 251 + Dead 252 + { 253 + node = node_id_of_string "node3"; 254 + incarnation = incarnation_of_int 3; 255 + declarator = node_id_of_string "node1"; 256 + }; 257 + ] 258 + in 259 + let packet = 260 + { cluster = "test"; primary = Ping { seq = 1; sender = node }; piggyback } 261 + in 262 + let buf = Cstruct.create 2000 in 263 + match encode_packet packet ~buf with 264 + | Error _ -> Alcotest.fail "encode failed" 265 + | Ok len -> ( 266 + let encoded = Cstruct.sub buf 0 len in 267 + match decode_packet encoded with 268 + | Ok decoded -> 269 + Alcotest.(check int) 270 + "piggyback count" 3 271 + (List.length decoded.piggyback) 272 + | Error e -> Alcotest.failf "decode failed: %s" (decode_error_to_string e) 273 + ) 274 + 275 + let qcheck_tests = 276 + List.map QCheck_alcotest.to_alcotest 277 + [ test_roundtrip_msg; test_roundtrip_packet; test_encoded_size_accurate ] 278 + 279 + let unit_tests = 280 + [ 281 + ("invalid_magic_rejected", `Quick, test_invalid_magic_rejected); 282 + ("unsupported_version_rejected", `Quick, test_unsupported_version_rejected); 283 + ("invalid_tag_rejected", `Quick, test_invalid_tag_rejected); 284 + ("encoder_write_byte", `Quick, test_encoder_write_byte); 285 + ("encoder_write_int16_be", `Quick, test_encoder_write_int16_be); 286 + ("encoder_write_int32_be", `Quick, test_encoder_write_int32_be); 287 + ("encoder_write_string", `Quick, test_encoder_write_string); 288 + ("encoder_write_empty_string", `Quick, test_encoder_write_empty_string); 289 + ("decoder_read_byte", `Quick, test_decoder_read_byte); 290 + ("decoder_read_int16_be", `Quick, test_decoder_read_int16_be); 291 + ("decoder_read_int32_be", `Quick, test_decoder_read_int32_be); 292 + ("decoder_read_string", `Quick, test_decoder_read_string); 293 + ("decoder_remaining", `Quick, test_decoder_remaining); 294 + ("decoder_is_empty", `Quick, test_decoder_is_empty); 295 + ("empty_piggyback", `Quick, test_empty_piggyback); 296 + ("multiple_piggyback", `Quick, test_multiple_piggyback); 297 + ] 298 + 299 + let () = 300 + Alcotest.run "codec" [ ("property", qcheck_tests); ("unit", unit_tests) ]
+168
test/test_crypto.ml
··· 1 + open Swim.Crypto 2 + 3 + let valid_key = String.make 32 '\x00' 4 + 5 + let test_roundtrip_property random = 6 + QCheck.Test.make ~count:500 ~name:"crypto roundtrip" Generators.arb_cstruct 7 + (fun plaintext -> 8 + match init_key valid_key with 9 + | Error _ -> false 10 + | Ok key -> ( 11 + let ciphertext = encrypt ~key ~random plaintext in 12 + match decrypt ~key ciphertext with 13 + | Ok decrypted -> Cstruct.equal plaintext decrypted 14 + | Error _ -> false)) 15 + 16 + let test_encrypt_increases_size random = 17 + QCheck.Test.make ~count:100 ~name:"encrypt increases size by overhead" 18 + Generators.arb_cstruct (fun plaintext -> 19 + match init_key valid_key with 20 + | Error _ -> false 21 + | Ok key -> 22 + let ciphertext = encrypt ~key ~random plaintext in 23 + Cstruct.length ciphertext = Cstruct.length plaintext + overhead) 24 + 25 + let test_different_plaintexts_different_ciphertexts random = 26 + QCheck.Test.make ~count:100 27 + ~name:"different plaintexts produce different ciphertexts" 28 + (QCheck.pair Generators.arb_cstruct Generators.arb_cstruct) (fun (p1, p2) -> 29 + if Cstruct.equal p1 p2 then true 30 + else 31 + match init_key valid_key with 32 + | Error _ -> false 33 + | Ok key -> 34 + let c1 = encrypt ~key ~random p1 in 35 + let c2 = encrypt ~key ~random p2 in 36 + not (Cstruct.equal c1 c2)) 37 + 38 + let test_init_key_valid_length () = 39 + match init_key (String.make 32 'a') with 40 + | Ok _ -> () 41 + | Error _ -> Alcotest.fail "expected valid key" 42 + 43 + let test_init_key_31_bytes_rejected () = 44 + match init_key (String.make 31 'a') with 45 + | Error `Invalid_key_length -> () 46 + | _ -> Alcotest.fail "expected Invalid_key_length" 47 + 48 + let test_init_key_33_bytes_rejected () = 49 + match init_key (String.make 33 'a') with 50 + | Error `Invalid_key_length -> () 51 + | _ -> Alcotest.fail "expected Invalid_key_length" 52 + 53 + let test_init_key_empty_rejected () = 54 + match init_key "" with 55 + | Error `Invalid_key_length -> () 56 + | _ -> Alcotest.fail "expected Invalid_key_length" 57 + 58 + let test_tampered_ciphertext_fails random () = 59 + match init_key valid_key with 60 + | Error _ -> Alcotest.fail "key init failed" 61 + | Ok key -> ( 62 + let plaintext = Cstruct.of_string "hello world" in 63 + let ciphertext = encrypt ~key ~random plaintext in 64 + let tampered = Cstruct.of_string (Cstruct.to_string ciphertext) in 65 + let pos = Cstruct.length tampered - 1 in 66 + Cstruct.set_uint8 tampered pos 67 + ((Cstruct.get_uint8 tampered pos + 1) land 0xFF); 68 + match decrypt ~key tampered with 69 + | Error `Decryption_failed -> () 70 + | _ -> Alcotest.fail "expected Decryption_failed") 71 + 72 + let test_truncated_ciphertext_fails random () = 73 + match init_key valid_key with 74 + | Error _ -> Alcotest.fail "key init failed" 75 + | Ok key -> ( 76 + let plaintext = Cstruct.of_string "hello world" in 77 + let ciphertext = encrypt ~key ~random plaintext in 78 + let truncated = Cstruct.sub ciphertext 0 (overhead - 1) in 79 + match decrypt ~key truncated with 80 + | Error `Too_short -> () 81 + | _ -> Alcotest.fail "expected Too_short") 82 + 83 + let test_wrong_key_fails random () = 84 + match (init_key valid_key, init_key (String.make 32 '\xFF')) with 85 + | Ok key1, Ok key2 -> ( 86 + let plaintext = Cstruct.of_string "secret message" in 87 + let ciphertext = encrypt ~key:key1 ~random plaintext in 88 + match decrypt ~key:key2 ciphertext with 89 + | Error `Decryption_failed -> () 90 + | _ -> Alcotest.fail "expected Decryption_failed") 91 + | _ -> Alcotest.fail "key init failed" 92 + 93 + let test_empty_plaintext random () = 94 + match init_key valid_key with 95 + | Error _ -> Alcotest.fail "key init failed" 96 + | Ok key -> ( 97 + let plaintext = Cstruct.empty in 98 + let ciphertext = encrypt ~key ~random plaintext in 99 + Alcotest.(check int) 100 + "ciphertext size" overhead 101 + (Cstruct.length ciphertext); 102 + match decrypt ~key ciphertext with 103 + | Ok decrypted -> 104 + Alcotest.(check int) "decrypted size" 0 (Cstruct.length decrypted) 105 + | Error _ -> Alcotest.fail "decrypt failed") 106 + 107 + let test_nonce_uniqueness random () = 108 + match init_key valid_key with 109 + | Error _ -> Alcotest.fail "key init failed" 110 + | Ok key -> 111 + let plaintext = Cstruct.of_string "test" in 112 + let c1 = encrypt ~key ~random plaintext in 113 + let c2 = encrypt ~key ~random plaintext in 114 + let nonce1 = Cstruct.sub c1 0 nonce_size in 115 + let nonce2 = Cstruct.sub c2 0 nonce_size in 116 + if Cstruct.equal nonce1 nonce2 then 117 + Alcotest.fail "nonces should be different" 118 + else () 119 + 120 + let test_ciphertext_differs_from_plaintext random () = 121 + match init_key valid_key with 122 + | Error _ -> Alcotest.fail "key init failed" 123 + | Ok key -> 124 + let plaintext = Cstruct.of_string "hello world secret" in 125 + let ciphertext = encrypt ~key ~random plaintext in 126 + let ciphertext_body = 127 + Cstruct.sub ciphertext nonce_size 128 + (Cstruct.length ciphertext - nonce_size) 129 + in 130 + if 131 + Cstruct.equal plaintext 132 + (Cstruct.sub ciphertext_body 0 133 + (min (Cstruct.length plaintext) (Cstruct.length ciphertext_body))) 134 + then Alcotest.fail "ciphertext should differ from plaintext" 135 + else () 136 + 137 + let () = 138 + Eio_main.run @@ fun env -> 139 + let random = Eio.Stdenv.secure_random env in 140 + let qcheck_tests = 141 + List.map QCheck_alcotest.to_alcotest 142 + [ 143 + test_roundtrip_property random; 144 + test_encrypt_increases_size random; 145 + test_different_plaintexts_different_ciphertexts random; 146 + ] 147 + in 148 + let unit_tests = 149 + [ 150 + ("init_key_valid_length", `Quick, test_init_key_valid_length); 151 + ("init_key_31_bytes_rejected", `Quick, test_init_key_31_bytes_rejected); 152 + ("init_key_33_bytes_rejected", `Quick, test_init_key_33_bytes_rejected); 153 + ("init_key_empty_rejected", `Quick, test_init_key_empty_rejected); 154 + ( "tampered_ciphertext_fails", 155 + `Quick, 156 + test_tampered_ciphertext_fails random ); 157 + ( "truncated_ciphertext_fails", 158 + `Quick, 159 + test_truncated_ciphertext_fails random ); 160 + ("wrong_key_fails", `Quick, test_wrong_key_fails random); 161 + ("empty_plaintext", `Quick, test_empty_plaintext random); 162 + ("nonce_uniqueness", `Quick, test_nonce_uniqueness random); 163 + ( "ciphertext_differs_from_plaintext", 164 + `Quick, 165 + test_ciphertext_differs_from_plaintext random ); 166 + ] 167 + in 168 + Alcotest.run "crypto" [ ("property", qcheck_tests); ("unit", unit_tests) ]
+457
test/test_pure.ml
··· 1 + open Swim.Types 2 + open Swim.Protocol_pure 3 + 4 + let node1 = 5 + make_node_info 6 + ~id:(node_id_of_string "node1") 7 + ~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7946)) 8 + ~meta:"" 9 + 10 + let node2 = 11 + make_node_info 12 + ~id:(node_id_of_string "node2") 13 + ~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\002", 7946)) 14 + ~meta:"" 15 + 16 + let node3 = 17 + make_node_info 18 + ~id:(node_id_of_string "node3") 19 + ~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\003", 7946)) 20 + ~meta:"" 21 + 22 + let now = Mtime.Span.of_uint64_ns 0L 23 + let alive_state : member_state = Alive 24 + let suspect_state : member_state = Suspect 25 + let dead_state : member_state = Dead 26 + 27 + let make_member ?(state = alive_state) ?(incarnation = 0) node = 28 + { 29 + node; 30 + state; 31 + incarnation = incarnation_of_int incarnation; 32 + state_change = now; 33 + } 34 + 35 + let test_alive_higher_incarnation_wins () = 36 + let member = make_member ~incarnation:1 node1 in 37 + let msg = Alive { node = node1; incarnation = incarnation_of_int 2 } in 38 + let result = handle_alive ~self:(node_id_of_string "self") member msg ~now in 39 + Alcotest.(check int) 40 + "incarnation" 2 41 + (incarnation_to_int result.new_state.incarnation); 42 + Alcotest.(check bool) "broadcast" true (List.length result.broadcasts = 1) 43 + 44 + let test_alive_lower_incarnation_ignored () = 45 + let member = make_member ~incarnation:5 node1 in 46 + let msg = Alive { node = node1; incarnation = incarnation_of_int 3 } in 47 + let result = handle_alive ~self:(node_id_of_string "self") member msg ~now in 48 + Alcotest.(check int) 49 + "incarnation unchanged" 5 50 + (incarnation_to_int result.new_state.incarnation); 51 + Alcotest.(check bool) "no broadcast" true (List.length result.broadcasts = 0) 52 + 53 + let test_alive_same_incarnation_unsuspects () = 54 + let member = make_member ~state:suspect_state ~incarnation:3 node1 in 55 + let msg = Alive { node = node1; incarnation = incarnation_of_int 3 } in 56 + let result = handle_alive ~self:(node_id_of_string "self") member msg ~now in 57 + Alcotest.(check string) 58 + "state alive" "alive" 59 + (member_state_to_string result.new_state.state) 60 + 61 + let test_alive_revives_dead_node () = 62 + let member = make_member ~state:dead_state ~incarnation:1 node1 in 63 + let msg = Alive { node = node1; incarnation = incarnation_of_int 5 } in 64 + let result = handle_alive ~self:(node_id_of_string "self") member msg ~now in 65 + Alcotest.(check string) 66 + "state alive" "alive" 67 + (member_state_to_string result.new_state.state); 68 + match result.events with 69 + | [ Join _ ] -> () 70 + | _ -> Alcotest.fail "expected Join event" 71 + 72 + let test_suspect_triggers_refute_for_self () = 73 + let member = make_member ~incarnation:1 node1 in 74 + let msg = 75 + Suspect 76 + { 77 + node = node_id_of_string "node1"; 78 + incarnation = incarnation_of_int 1; 79 + suspector = node_id_of_string "node2"; 80 + } 81 + in 82 + let result = 83 + handle_suspect ~self:(node_id_of_string "node1") member msg ~now 84 + in 85 + Alcotest.(check int) 86 + "incarnation incremented" 2 87 + (incarnation_to_int result.new_state.incarnation); 88 + match result.broadcasts with 89 + | [ Alive { incarnation; _ } ] -> 90 + Alcotest.(check int) 91 + "refute incarnation" 2 92 + (incarnation_to_int incarnation) 93 + | _ -> Alcotest.fail "expected Alive refute broadcast" 94 + 95 + let test_suspect_higher_incarnation_suspects () = 96 + let member = make_member ~state:alive_state ~incarnation:1 node1 in 97 + let msg = 98 + Suspect 99 + { 100 + node = node_id_of_string "node1"; 101 + incarnation = incarnation_of_int 2; 102 + suspector = node_id_of_string "node2"; 103 + } 104 + in 105 + let result = 106 + handle_suspect ~self:(node_id_of_string "self") member msg ~now 107 + in 108 + Alcotest.(check string) 109 + "state suspect" "suspect" 110 + (member_state_to_string result.new_state.state) 111 + 112 + let test_suspect_lower_incarnation_ignored () = 113 + let member = make_member ~state:alive_state ~incarnation:5 node1 in 114 + let msg = 115 + Suspect 116 + { 117 + node = node_id_of_string "node1"; 118 + incarnation = incarnation_of_int 3; 119 + suspector = node_id_of_string "node2"; 120 + } 121 + in 122 + let result = 123 + handle_suspect ~self:(node_id_of_string "self") member msg ~now 124 + in 125 + Alcotest.(check string) 126 + "state unchanged" "alive" 127 + (member_state_to_string result.new_state.state); 128 + Alcotest.(check bool) "no broadcast" true (List.length result.broadcasts = 0) 129 + 130 + let test_suspect_dead_node_ignored () = 131 + let member = make_member ~state:dead_state ~incarnation:1 node1 in 132 + let msg = 133 + Suspect 134 + { 135 + node = node_id_of_string "node1"; 136 + incarnation = incarnation_of_int 5; 137 + suspector = node_id_of_string "node2"; 138 + } 139 + in 140 + let result = 141 + handle_suspect ~self:(node_id_of_string "self") member msg ~now 142 + in 143 + Alcotest.(check string) 144 + "state dead" "dead" 145 + (member_state_to_string result.new_state.state) 146 + 147 + let test_dead_marks_node_dead () = 148 + let member = make_member ~state:alive_state ~incarnation:1 node1 in 149 + let msg = 150 + Dead 151 + { 152 + node = node_id_of_string "node1"; 153 + incarnation = incarnation_of_int 2; 154 + declarator = node_id_of_string "node2"; 155 + } 156 + in 157 + let result = handle_dead member msg ~now in 158 + Alcotest.(check string) 159 + "state dead" "dead" 160 + (member_state_to_string result.new_state.state); 161 + match result.events with 162 + | [ Leave _ ] -> () 163 + | _ -> Alcotest.fail "expected Leave event" 164 + 165 + let test_dead_already_dead_ignored () = 166 + let member = make_member ~state:dead_state ~incarnation:5 node1 in 167 + let msg = 168 + Dead 169 + { 170 + node = node_id_of_string "node1"; 171 + incarnation = incarnation_of_int 10; 172 + declarator = node_id_of_string "node2"; 173 + } 174 + in 175 + let result = handle_dead member msg ~now in 176 + Alcotest.(check bool) "no events" true (List.length result.events = 0) 177 + 178 + let test_dead_lower_incarnation_ignored () = 179 + let member = make_member ~state:alive_state ~incarnation:10 node1 in 180 + let msg = 181 + Dead 182 + { 183 + node = node_id_of_string "node1"; 184 + incarnation = incarnation_of_int 5; 185 + declarator = node_id_of_string "node2"; 186 + } 187 + in 188 + let result = handle_dead member msg ~now in 189 + Alcotest.(check string) 190 + "state alive" "alive" 191 + (member_state_to_string result.new_state.state) 192 + 193 + let test_invalidates_dead_beats_all () = 194 + let dead_msg = 195 + Dead 196 + { 197 + node = node_id_of_string "node1"; 198 + incarnation = incarnation_of_int 1; 199 + declarator = node_id_of_string "node2"; 200 + } 201 + in 202 + let alive_msg = Alive { node = node1; incarnation = incarnation_of_int 5 } in 203 + let suspect_msg = 204 + Suspect 205 + { 206 + node = node_id_of_string "node1"; 207 + incarnation = incarnation_of_int 5; 208 + suspector = node_id_of_string "node2"; 209 + } 210 + in 211 + Alcotest.(check bool) 212 + "dead invalidates alive" true 213 + (invalidates ~newer:dead_msg ~older:alive_msg); 214 + Alcotest.(check bool) 215 + "dead invalidates suspect" true 216 + (invalidates ~newer:dead_msg ~older:suspect_msg) 217 + 218 + let test_invalidates_alive_beats_suspect_same_inc () = 219 + let alive_msg = Alive { node = node1; incarnation = incarnation_of_int 5 } in 220 + let suspect_msg = 221 + Suspect 222 + { 223 + node = node_id_of_string "node1"; 224 + incarnation = incarnation_of_int 5; 225 + suspector = node_id_of_string "node2"; 226 + } 227 + in 228 + Alcotest.(check bool) 229 + "alive beats suspect" true 230 + (invalidates ~newer:alive_msg ~older:suspect_msg) 231 + 232 + let test_invalidates_higher_incarnation_wins () = 233 + let alive_old = Alive { node = node1; incarnation = incarnation_of_int 1 } in 234 + let alive_new = Alive { node = node1; incarnation = incarnation_of_int 2 } in 235 + Alcotest.(check bool) 236 + "higher inc wins" true 237 + (invalidates ~newer:alive_new ~older:alive_old); 238 + Alcotest.(check bool) 239 + "lower inc doesnt" false 240 + (invalidates ~newer:alive_old ~older:alive_new) 241 + 242 + let test_invalidates_different_nodes_false () = 243 + let alive1 = Alive { node = node1; incarnation = incarnation_of_int 10 } in 244 + let alive2 = Alive { node = node2; incarnation = incarnation_of_int 1 } in 245 + Alcotest.(check bool) 246 + "different nodes" false 247 + (invalidates ~newer:alive1 ~older:alive2) 248 + 249 + let test_merge_dead_local_wins () = 250 + let local = make_member ~state:dead_state ~incarnation:1 node1 in 251 + let remote = make_member ~state:alive_state ~incarnation:10 node1 in 252 + let result = merge_member_state ~local ~remote in 253 + Alcotest.(check string) 254 + "local dead wins" "dead" 255 + (member_state_to_string result.state) 256 + 257 + let test_merge_remote_dead_higher_inc_wins () = 258 + let local = make_member ~state:alive_state ~incarnation:1 node1 in 259 + let remote = make_member ~state:dead_state ~incarnation:5 node1 in 260 + let result = merge_member_state ~local ~remote in 261 + Alcotest.(check string) 262 + "remote dead wins" "dead" 263 + (member_state_to_string result.state) 264 + 265 + let test_merge_higher_incarnation_wins () = 266 + let local = make_member ~state:alive_state ~incarnation:1 node1 in 267 + let remote = make_member ~state:alive_state ~incarnation:5 node1 in 268 + let result = merge_member_state ~local ~remote in 269 + Alcotest.(check int) "higher inc" 5 (incarnation_to_int result.incarnation) 270 + 271 + let test_merge_suspect_beats_alive_higher_inc () = 272 + let local = make_member ~state:alive_state ~incarnation:1 node1 in 273 + let remote = make_member ~state:suspect_state ~incarnation:5 node1 in 274 + let result = merge_member_state ~local ~remote in 275 + Alcotest.(check string) 276 + "suspect" "suspect" 277 + (member_state_to_string result.state) 278 + 279 + let test_merge_alive_beats_suspect_same_or_higher_inc () = 280 + let local = make_member ~state:suspect_state ~incarnation:3 node1 in 281 + let remote = make_member ~state:alive_state ~incarnation:3 node1 in 282 + let result = merge_member_state ~local ~remote in 283 + Alcotest.(check string) "alive" "alive" (member_state_to_string result.state) 284 + 285 + let test_suspicion_timeout_increases_with_nodes () = 286 + let config = default_config in 287 + let t1 = suspicion_timeout config ~node_count:10 in 288 + let t2 = suspicion_timeout config ~node_count:100 in 289 + Alcotest.(check bool) "more nodes = longer timeout" true (t2 > t1) 290 + 291 + let test_suspicion_timeout_bounded () = 292 + let config = { default_config with suspicion_max_timeout = 30.0 } in 293 + let t = suspicion_timeout config ~node_count:1000000 in 294 + Alcotest.(check bool) "bounded" true (t <= 30.0) 295 + 296 + let test_suspicion_timeout_zero_nodes () = 297 + let config = default_config in 298 + let t = suspicion_timeout config ~node_count:0 in 299 + Alcotest.(check bool) "handles zero" true (t >= 0.0) 300 + 301 + let test_retransmit_limit_increases_with_nodes () = 302 + let config = default_config in 303 + let r1 = retransmit_limit config ~node_count:10 in 304 + let r2 = retransmit_limit config ~node_count:100 in 305 + Alcotest.(check bool) "more nodes = higher limit" true (r2 > r1) 306 + 307 + let test_next_probe_empty_list () = 308 + let result = 309 + next_probe_target ~self:(node_id_of_string "self") ~probe_index:0 310 + ~members:[] 311 + in 312 + match result with None -> () | Some _ -> Alcotest.fail "expected None" 313 + 314 + let test_next_probe_skips_self () = 315 + let self = node_id_of_string "node1" in 316 + let result = 317 + next_probe_target ~self ~probe_index:0 ~members:[ node1; node2 ] 318 + in 319 + match result with 320 + | Some (target, _) -> 321 + Alcotest.(check bool) "not self" false (equal_node_id target.id self) 322 + | None -> Alcotest.fail "expected Some" 323 + 324 + let test_next_probe_wraps_around () = 325 + let self = node_id_of_string "self" in 326 + let result = 327 + next_probe_target ~self ~probe_index:5 ~members:[ node1; node2 ] 328 + in 329 + match result with Some _ -> () | None -> Alcotest.fail "expected Some" 330 + 331 + let test_next_probe_all_self_returns_none () = 332 + let self = node_id_of_string "node1" in 333 + let result = next_probe_target ~self ~probe_index:0 ~members:[ node1 ] in 334 + match result with 335 + | None -> () 336 + | Some _ -> Alcotest.fail "expected None when only self" 337 + 338 + let test_select_indirect_targets_excludes_self_and_target () = 339 + let self = node_id_of_string "node1" in 340 + let exclude = node_id_of_string "node2" in 341 + let result = 342 + select_indirect_targets ~self ~exclude ~count:10 343 + ~members:[ node1; node2; node3 ] 344 + in 345 + Alcotest.(check int) "only node3" 1 (List.length result); 346 + Alcotest.(check bool) 347 + "is node3" true 348 + (equal_node_id (List.hd result).id node3.id) 349 + 350 + let test_select_indirect_targets_limits_count () = 351 + let self = node_id_of_string "self" in 352 + let exclude = node_id_of_string "exclude" in 353 + let result = 354 + select_indirect_targets ~self ~exclude ~count:1 355 + ~members:[ node1; node2; node3 ] 356 + in 357 + Alcotest.(check int) "limited to 1" 1 (List.length result) 358 + 359 + let clamp_incarnation inc = 360 + incarnation_of_int (incarnation_to_int inc land 0x7FFFFFFF) 361 + 362 + let test_merge_converges = 363 + QCheck.Test.make ~count:200 364 + ~name:"merge converges (applying twice yields same result)" 365 + (QCheck.pair Generators.arb_member_snapshot Generators.arb_member_snapshot) 366 + (fun (a, b) -> 367 + let a = 368 + { a with node = node1; incarnation = clamp_incarnation a.incarnation } 369 + in 370 + let b = 371 + { b with node = node1; incarnation = clamp_incarnation b.incarnation } 372 + in 373 + let ab = merge_member_state ~local:a ~remote:b in 374 + let ab2 = merge_member_state ~local:ab ~remote:b in 375 + ab.state = ab2.state && ab.incarnation = ab2.incarnation) 376 + 377 + let test_merge_idempotent = 378 + QCheck.Test.make ~count:200 ~name:"merge is idempotent" 379 + Generators.arb_member_snapshot (fun a -> 380 + let result = merge_member_state ~local:a ~remote:a in 381 + result.state = a.state && result.incarnation = a.incarnation) 382 + 383 + let qcheck_tests = 384 + List.map QCheck_alcotest.to_alcotest 385 + [ test_merge_converges; test_merge_idempotent ] 386 + 387 + let unit_tests = 388 + [ 389 + ("alive_higher_incarnation_wins", `Quick, test_alive_higher_incarnation_wins); 390 + ( "alive_lower_incarnation_ignored", 391 + `Quick, 392 + test_alive_lower_incarnation_ignored ); 393 + ( "alive_same_incarnation_unsuspects", 394 + `Quick, 395 + test_alive_same_incarnation_unsuspects ); 396 + ("alive_revives_dead_node", `Quick, test_alive_revives_dead_node); 397 + ( "suspect_triggers_refute_for_self", 398 + `Quick, 399 + test_suspect_triggers_refute_for_self ); 400 + ( "suspect_higher_incarnation_suspects", 401 + `Quick, 402 + test_suspect_higher_incarnation_suspects ); 403 + ( "suspect_lower_incarnation_ignored", 404 + `Quick, 405 + test_suspect_lower_incarnation_ignored ); 406 + ("suspect_dead_node_ignored", `Quick, test_suspect_dead_node_ignored); 407 + ("dead_marks_node_dead", `Quick, test_dead_marks_node_dead); 408 + ("dead_already_dead_ignored", `Quick, test_dead_already_dead_ignored); 409 + ( "dead_lower_incarnation_ignored", 410 + `Quick, 411 + test_dead_lower_incarnation_ignored ); 412 + ("invalidates_dead_beats_all", `Quick, test_invalidates_dead_beats_all); 413 + ( "invalidates_alive_beats_suspect_same_inc", 414 + `Quick, 415 + test_invalidates_alive_beats_suspect_same_inc ); 416 + ( "invalidates_higher_incarnation_wins", 417 + `Quick, 418 + test_invalidates_higher_incarnation_wins ); 419 + ( "invalidates_different_nodes_false", 420 + `Quick, 421 + test_invalidates_different_nodes_false ); 422 + ("merge_dead_local_wins", `Quick, test_merge_dead_local_wins); 423 + ( "merge_remote_dead_higher_inc_wins", 424 + `Quick, 425 + test_merge_remote_dead_higher_inc_wins ); 426 + ("merge_higher_incarnation_wins", `Quick, test_merge_higher_incarnation_wins); 427 + ( "merge_suspect_beats_alive_higher_inc", 428 + `Quick, 429 + test_merge_suspect_beats_alive_higher_inc ); 430 + ( "merge_alive_beats_suspect_same_or_higher_inc", 431 + `Quick, 432 + test_merge_alive_beats_suspect_same_or_higher_inc ); 433 + ( "suspicion_timeout_increases_with_nodes", 434 + `Quick, 435 + test_suspicion_timeout_increases_with_nodes ); 436 + ("suspicion_timeout_bounded", `Quick, test_suspicion_timeout_bounded); 437 + ("suspicion_timeout_zero_nodes", `Quick, test_suspicion_timeout_zero_nodes); 438 + ( "retransmit_limit_increases_with_nodes", 439 + `Quick, 440 + test_retransmit_limit_increases_with_nodes ); 441 + ("next_probe_empty_list", `Quick, test_next_probe_empty_list); 442 + ("next_probe_skips_self", `Quick, test_next_probe_skips_self); 443 + ("next_probe_wraps_around", `Quick, test_next_probe_wraps_around); 444 + ( "next_probe_all_self_returns_none", 445 + `Quick, 446 + test_next_probe_all_self_returns_none ); 447 + ( "select_indirect_targets_excludes", 448 + `Quick, 449 + test_select_indirect_targets_excludes_self_and_target ); 450 + ( "select_indirect_targets_limits", 451 + `Quick, 452 + test_select_indirect_targets_limits_count ); 453 + ] 454 + 455 + let () = 456 + Alcotest.run "protocol_pure" 457 + [ ("property", qcheck_tests); ("unit", unit_tests) ]