diff --git a/crates/irtt-cli/tests/client_multi_target.rs b/crates/irtt-cli/tests/client_multi_target.rs index d6f39af..a5fca2b 100644 --- a/crates/irtt-cli/tests/client_multi_target.rs +++ b/crates/irtt-cli/tests/client_multi_target.rs @@ -206,42 +206,6 @@ fn single_target_ten_millisecond_cadence_uses_managed_deadlines() { .any(|line| line.starts_with("session_closed,"))); } -#[test] -fn finite_single_target_peer_close_exits_successfully() { - let server = start_peer_close_server(test_params( - Some(Duration::from_millis(40)), - Duration::from_millis(10), - )); - let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); - command.args([ - "--duration", - "40ms", - "--interval", - "10ms", - "--format", - "csv", - "--columns", - "seq", - "--header", - "never", - &server.addr.to_string(), - ]); - - let output = run_with_timeout(command, Duration::from_secs(3)); - server.join(); - - assert!( - output.status.success(), - "status={:?}\nstderr={}", - output.status.code(), - String::from_utf8_lossy(&output.stderr) - ); - let stdout = String::from_utf8(output.stdout).unwrap(); - let stderr = String::from_utf8(output.stderr).unwrap(); - assert!(!stdout.trim().is_empty(), "{stdout}"); - assert!(!stderr.contains("peer closure"), "{stderr}"); -} - #[test] fn continuous_single_target_peer_close_exits_nonzero() { let server = start_peer_close_server(test_params(None, Duration::from_millis(10))); diff --git a/crates/irtt-client/src/managed/tests.rs b/crates/irtt-client/src/managed/tests.rs index dc91677..0a52cee 100644 --- a/crates/irtt-client/src/managed/tests.rs +++ b/crates/irtt-client/src/managed/tests.rs @@ -802,7 +802,7 @@ fn terminal_targets_count_toward_retained_generation_limit() { } #[test] -fn pruning_preserves_target_cursor_progress() { +fn pruning_leaves_no_stale_target_work() { let (mut task, _) = ManagedClient::task( config(ManagedPacing::Staggered), vec![ @@ -812,29 +812,39 @@ fn pruning_preserves_target_cursor_progress() { ], ) .unwrap(); - task.cursor = 5; - task.timeout_cursor = 5; - task.send_cursor = 5; + task.cursor = 2; + task.timeout_cursor = 2; + task.send_cursor = 2; task.scan_remaining = 3; task.burst_remaining = 3; task.stagger_remaining = 3; - task.targets.truncate(2); - task.rebase_target_cursors(); - assert_eq!(task.cursor, 1); - assert_eq!(task.timeout_cursor, 1); - assert_eq!(task.send_cursor, 1); - assert_eq!(task.scan_remaining, 2); - assert_eq!(task.burst_remaining, 2); - assert_eq!(task.stagger_remaining, 2); - task.targets.clear(); - task.rebase_target_cursors(); - assert_eq!(task.cursor, 0); - assert_eq!(task.timeout_cursor, 0); - assert_eq!(task.send_cursor, 0); + task.targets[2].desired = false; + task.targets[2].state = TargetState::Terminal; + task.prune_undesired_terminal(); + + // Timeout discovery indexes `targets` with `timeout_cursor` directly, so a + // prune must leave it safe to use; the target and send passes modulo their + // own cursors. Pending per-pass work must not outlive the targets it was + // scheduled for either. + assert_eq!(task.targets.len(), 2); + assert!(task.timeout_cursor < task.targets.len()); + assert!(task.scan_remaining <= task.targets.len()); + assert!(task.burst_remaining <= task.targets.len()); + assert!(task.stagger_remaining <= task.targets.len()); + assert!(!task.poll_timeout_pass(Instant::now())); + + for target in &mut task.targets { + target.desired = false; + target.state = TargetState::Terminal; + } + task.prune_undesired_terminal(); + + assert!(task.targets.is_empty()); assert_eq!(task.scan_remaining, 0); assert_eq!(task.burst_remaining, 0); assert_eq!(task.stagger_remaining, 0); + assert!(!task.poll_timeout_pass(Instant::now())); } #[test] @@ -1761,91 +1771,6 @@ fn timeout_discovery_inspects_at_most_one_budget_of_targets() { assert!(!task.poll_timeout_pass(Instant::now())); assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET); - assert_eq!(task.timeout_cursor, TIMEOUT_WORK_BUDGET); -} - -#[test] -fn idle_timeout_discovery_inspects_one_target_once() { - let (mut task, _) = ManagedClient::task( - config(ManagedPacing::Burst), - vec![ManagedTargetConfig::new("idle", "127.0.0.1:9")], - ) - .unwrap(); - - task.timeout_inspections = 0; - assert!(!task.poll_timeout_pass(Instant::now())); - - assert_eq!(task.timeout_inspections, 1); - assert_eq!(task.timeout_cursor, 0); -} - -#[test] -fn idle_timeout_discovery_sweeps_small_target_set_once() { - let target_count = 8; - let mut managed = config(ManagedPacing::Burst); - managed.max_live_target_generations = target_count; - let targets = (0..target_count) - .map(|index| ManagedTargetConfig::new(format!("idle-{index}"), "127.0.0.1:9")) - .collect(); - let (mut task, _) = ManagedClient::task(managed, targets).unwrap(); - - task.timeout_inspections = 0; - assert!(!task.poll_timeout_pass(Instant::now())); - - assert_eq!(task.timeout_inspections, target_count); - assert_eq!(task.timeout_cursor, 0); -} - -#[test] -fn sparse_timeout_backlog_progresses_within_one_poll() { - let server = start_server(ServerBehavior::Echo, None); - let target_count = TIMEOUT_WORK_BUDGET + 1; - let late_index = target_count - 1; - let mut managed = config(ManagedPacing::Burst); - managed.completion = ManagedCompletionPolicy::ExplicitStop; - managed.client.duration = None; - managed.max_live_target_generations = target_count; - let targets = (0..target_count) - .map(|index| { - if index == late_index { - target("late", server.addr) - } else { - ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9") - } - }) - .collect(); - let (mut task, _) = ManagedClient::task(managed, targets).unwrap(); - for target in &mut task.targets[..late_index] { - target.state = TargetState::Terminal; - } - let observations = Arc::new(Mutex::new(Vec::new())); - task.event_observations = Some(Arc::clone(&observations)); - let runtime = runtime(); - open_target_for_timeout_test(&runtime, &mut task, late_index); - let now = Instant::now(); - for seq in 0..u32::try_from(TIMEOUT_WORK_BUDGET + 64).unwrap() { - replace_pending_for_timeout_test( - &mut task, - late_index, - timeout_probe( - seq, - now - Duration::from_secs(2), - now - Duration::from_secs(1), - ), - ); - } - - assert!(!task.poll_timeout_pass(now)); - assert!(task.poll_timeout_pass(now)); - - assert_eq!( - timeout_losses_for(&observations, "late"), - TIMEOUT_WORK_BUDGET - ); - assert_eq!(task.timeout_backlog.len(), 1); - close_timeout_test_target(&runtime, &mut task, late_index); - drop(task); - server.finish(); } #[test] @@ -1889,7 +1814,6 @@ fn sparse_timeout_backlog_persists_across_polls() { TIMEOUT_WORK_BUDGET ); assert_eq!(task.timeout_backlog.len(), 1); - assert_eq!(task.timeout_cursor, TIMEOUT_WORK_BUDGET); assert!(task.poll_timeout_pass(now)); assert_eq!( @@ -1897,75 +1821,11 @@ fn sparse_timeout_backlog_persists_across_polls() { 2 * TIMEOUT_WORK_BUDGET ); assert_eq!(task.timeout_backlog.len(), 1); - assert_eq!(task.timeout_cursor, 2 * TIMEOUT_WORK_BUDGET); close_timeout_test_target(&runtime, &mut task, 0); drop(task); server.finish(); } -#[test] -fn timeout_backlog_round_robins_known_sparse_targets() { - let first = start_server(ServerBehavior::Echo, None); - let second = start_server(ServerBehavior::Echo, None); - let target_count = TIMEOUT_WORK_BUDGET + 2; - let mut managed = config(ManagedPacing::Burst); - managed.completion = ManagedCompletionPolicy::ExplicitStop; - managed.client.duration = None; - managed.max_live_target_generations = target_count; - let targets = vec![target("first", first.addr), target("second", second.addr)] - .into_iter() - .chain( - (2..target_count) - .map(|index| ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9")), - ) - .collect(); - let (mut task, _) = ManagedClient::task(managed, targets).unwrap(); - for target in &mut task.targets[2..] { - target.state = TargetState::Terminal; - } - let observations = Arc::new(Mutex::new(Vec::new())); - task.event_observations = Some(Arc::clone(&observations)); - let runtime = runtime(); - open_target_for_timeout_test(&runtime, &mut task, 0); - open_target_for_timeout_test(&runtime, &mut task, 1); - let now = Instant::now(); - for index in 0..2 { - for seq in 0..u32::try_from(TIMEOUT_WORK_BUDGET + 1).unwrap() { - replace_pending_for_timeout_test( - &mut task, - index, - timeout_probe( - seq, - now - Duration::from_secs(2), - now - Duration::from_secs(1), - ), - ); - } - task.timeout_backlog.push_back(TimeoutBacklogEntry { - index, - generation: task.targets[index].instance.generation, - }); - } - task.timeout_cursor = 2; - - assert!(task.poll_timeout_pass(now)); - - assert_eq!( - timeout_losses_for(&observations, "first"), - TIMEOUT_WORK_BUDGET / 2 - ); - assert_eq!( - timeout_losses_for(&observations, "second"), - TIMEOUT_WORK_BUDGET / 2 - ); - assert_eq!(task.timeout_backlog.len(), 2); - close_timeout_test_target(&runtime, &mut task, 0); - close_timeout_test_target(&runtime, &mut task, 1); - drop(task); - first.finish(); - second.finish(); -} - #[test] fn timeout_backlog_does_not_starve_discovery() { let first = start_server(ServerBehavior::Echo, None); @@ -2021,11 +1881,9 @@ fn timeout_backlog_does_not_starve_discovery() { assert!(task.poll_timeout_pass(now)); assert_eq!(timeout_losses_for(&observations, "late"), 0); - assert_eq!(task.timeout_cursor, TIMEOUT_WORK_BUDGET + 1); assert!(task.poll_timeout_pass(now)); assert_eq!(timeout_losses_for(&observations, "late"), 1); - assert_eq!(task.timeout_cursor, 0); assert_eq!(task.timeout_backlog.len(), 1); close_timeout_test_target(&runtime, &mut task, 0); close_timeout_test_target(&runtime, &mut task, late_index); @@ -2166,7 +2024,6 @@ fn timeout_discovery_reaches_later_target_after_budget_slice() { task.timeout_inspections = 0; assert!(!task.poll_timeout_pass(now)); assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET); - assert_eq!(task.timeout_cursor, late_index); assert_eq!(timeout_losses_for(&observations, "late"), 0); task.timeout_inspections = 0; diff --git a/crates/irtt-client/src/session/machine.rs b/crates/irtt-client/src/session/machine.rs index 7eade8b..21550fb 100644 --- a/crates/irtt-client/src/session/machine.rs +++ b/crates/irtt-client/src/session/machine.rs @@ -1060,7 +1060,6 @@ impl SessionMachine { #[cfg(test)] mod tests { use super::*; - use crate::client::{schedule::ProbeSchedule, validate_datagram_length}; fn open_machine(max_pending_probes: usize, probe_timeout: Duration) -> SessionMachine { let config = ClientConfig { @@ -1584,25 +1583,22 @@ mod tests { } #[test] - fn dropping_prepared_probe_changes_nothing() { - let machine = open_machine(4, Duration::from_secs(1)); - let prepared = machine.prepare_probe().unwrap().unwrap(); - assert_eq!(prepared.seq, 0); - drop(prepared); - - let session = active(&machine); - assert_eq!(session.next_wire_seq, 0); - assert_eq!(session.packets_sent, 0); - assert_eq!(session.pending.len(), 0); - } - - #[test] - fn repeated_uncommitted_preflight_changes_no_logical_state() { + fn uncommitted_probe_preparation_changes_no_authoritative_state() { let mut machine = open_machine(4, Duration::from_secs(1)); let prepared = machine.prepare_probe().unwrap().unwrap(); + assert_eq!(prepared.seq, 0); - let _first_preflight = machine.preflight_probe_commit(&prepared).unwrap(); - let _second_preflight = machine.preflight_probe_commit(&prepared).unwrap(); + // Repeating preflight, and finalizing a commit that is never applied, + // must both leave the session exactly as it was. + { + let _discarded_preflight = machine.preflight_probe_commit(&prepared).unwrap(); + } + { + let preflight = machine.preflight_probe_commit(&prepared).unwrap(); + let _discarded_commit = machine + .finalize_probe_commit(preflight, timestamp(Instant::now())) + .unwrap(); + } let session = active(&machine); assert_eq!(session.next_wire_seq, 0); @@ -1636,66 +1632,6 @@ mod tests { assert_eq!(session.pending.capacity(), capacity); } - #[test] - fn simulated_send_error_leaves_machine_and_schedule_unchanged() { - let mut machine = open_machine(4, Duration::from_secs(1)); - let start = Instant::now(); - let mut schedule = ProbeSchedule::new(start, &active(&machine).negotiated).unwrap(); - assert!(schedule.permit_probe_at(start)); - let prepared = machine.prepare_probe().unwrap().unwrap(); - let sent_at = timestamp(start); - let machine_preflight = machine.preflight_probe_commit(&prepared).unwrap(); - let schedule_commit = schedule - .preflight_caller_commit(machine_preflight.next_packets_sent) - .unwrap(); - let machine_commit = machine - .finalize_probe_commit(machine_preflight, sent_at) - .unwrap(); - - let _uncommitted_machine = machine_commit; - let _uncommitted_schedule = schedule_commit; - - let session = active(&machine); - assert_eq!(session.next_wire_seq, 0); - assert_eq!(session.packets_sent, 0); - assert_eq!(session.pending.len(), 0); - assert_eq!(schedule.next_send_deadline(), Some(start)); - } - - #[test] - fn short_success_commits_before_reporting_transport_invariant() { - let mut machine = open_machine(4, Duration::from_secs(1)); - let start = Instant::now(); - let mut schedule = ProbeSchedule::new(start, &active(&machine).negotiated).unwrap(); - assert!(schedule.permit_probe_at(start)); - let prepared = machine.prepare_probe().unwrap().unwrap(); - let machine_preflight = machine.preflight_probe_commit(&prepared).unwrap(); - let machine_commit = machine - .finalize_probe_commit(machine_preflight, timestamp(start)) - .unwrap(); - let schedule_commit = schedule.preflight_managed_commit(start, start).unwrap(); - let expected = prepared.bytes.len(); - let actual = expected - 1; - - machine.commit_probe_sent(machine_commit, actual); - schedule.commit(schedule_commit); - let result = validate_datagram_length(expected, actual); - - assert!(matches!( - result, - Err(ClientError::DatagramLengthMismatch { expected, actual }) - if actual + 1 == expected - )); - let session = active(&machine); - assert_eq!(session.next_wire_seq, 1); - assert_eq!(session.packets_sent, 1); - assert_eq!(session.pending.len(), 1); - assert_eq!( - schedule.next_send_deadline(), - Some(start + machine.config.interval) - ); - } - #[test] fn repeated_would_block_style_preflight_commits_once() { let mut machine = open_machine(4, Duration::from_secs(1)); @@ -1737,26 +1673,6 @@ mod tests { assert_eq!(sent.bytes, prepared.bytes.len()); } - #[test] - fn dropping_preflight_or_finalized_commit_changes_no_authoritative_state() { - let mut machine = open_machine(4, Duration::from_secs(1)); - let prepared = machine.prepare_probe().unwrap().unwrap(); - { - let _preflight = machine.preflight_probe_commit(&prepared).unwrap(); - } - { - let preflight = machine.preflight_probe_commit(&prepared).unwrap(); - let _commit = machine - .finalize_probe_commit(preflight, timestamp(Instant::now())) - .unwrap(); - } - - let session = active(&machine); - assert_eq!(session.next_wire_seq, 0); - assert_eq!(session.packets_sent, 0); - assert_eq!(session.pending.len(), 0); - } - #[test] fn commit_uses_reserved_capacity_and_prevalidated_counter() { let mut machine = open_machine(2, Duration::from_secs(1)); diff --git a/crates/irtt-client/tests/large_packets.rs b/crates/irtt-client/tests/large_packets.rs index 8821c01..1e1d47a 100644 --- a/crates/irtt-client/tests/large_packets.rs +++ b/crates/irtt-client/tests/large_packets.rs @@ -39,37 +39,6 @@ fn assert_reply_bytes(event: &ClientEvent, expected: usize) { } } -// ─── Config/client validation ─── - -#[test] -fn length_zero_baseline() { - let params = params_with_length(0); - let expected = test_echo_packet_len(false, ¶ms); - let header = echo_header_len(false, ¶ms); - assert_eq!(expected, header); -} - -#[test] -fn length_one_accepted() { - let params = params_with_length(1); - let expected = test_echo_packet_len(false, ¶ms); - assert!(expected >= 1); -} - -#[test] -fn length_1472_accepted() { - let params = params_with_length(1472); - let expected = test_echo_packet_len(false, ¶ms); - assert_eq!(expected, 1472); -} - -#[test] -fn length_4096_accepted() { - let params = params_with_length(4096); - let expected = test_echo_packet_len(false, ¶ms); - assert_eq!(expected, 4096); -} - // ─── Echo request size: FakeServer observes exact packet size ─── #[test] @@ -98,21 +67,13 @@ fn echo_request_size_matches_echo_packet_len_above_header() { // ─── Echo reply handling ─── -#[test] -fn echo_reply_decoded_for_large_length() { - let params = params_with_length(4096); - let run = run_one_probe(params.clone(), TimestampFields::default()); - let expected = test_echo_packet_len(false, ¶ms); - assert_reply(&run.reply); - assert_reply_bytes(&run.reply, expected); -} - #[test] fn echo_reply_bytes_consistent_with_echo_packet_len_for_various_lengths() { for length in [0, 1472, 4096] { let params = params_with_length(length); let run = run_one_probe(params.clone(), TimestampFields::default()); let expected = test_echo_packet_len(false, ¶ms); + assert_reply(&run.reply); assert_reply_bytes(&run.reply, expected); } } diff --git a/crates/irtt-client/tests/protocol_options.rs b/crates/irtt-client/tests/protocol_options.rs index ac0f059..83be36f 100644 --- a/crates/irtt-client/tests/protocol_options.rs +++ b/crates/irtt-client/tests/protocol_options.rs @@ -369,16 +369,3 @@ fn assert_one_way_presence( assert_eq!(sample.client_to_server.is_some(), client_to_server); assert_eq!(sample.server_to_client.is_some(), server_to_client); } - -#[test] -fn dscp_configured_open_close_smoke() { - let mut params = default_params(); - params.dscp = 46; - let server = start_open_server(params.clone(), None); - let mut client = Client::connect(config_for_params(server.addr, ¶ms)).unwrap(); - - let outcome = client.open().unwrap(); - assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. })); - client.close().unwrap(); - server.join(); -} diff --git a/crates/irtt-client/tests/protocol_options/backend_smoke.rs b/crates/irtt-client/tests/protocol_options/backend_smoke.rs index ffd040a..3607b9e 100644 --- a/crates/irtt-client/tests/protocol_options/backend_smoke.rs +++ b/crates/irtt-client/tests/protocol_options/backend_smoke.rs @@ -22,27 +22,6 @@ fn backend_basic_open_echo_close() { client.close().unwrap(); } -#[test] -fn backend_ttl_smoke() { - let params = default_params(); - let peer = BackendPeer::start_open_echo(params.clone(), None); - let mut config = config_for_params(peer.addr(), ¶ms); - config.socket_config.ttl = Some(64); - let mut client = Client::connect(config).unwrap(); - - let outcome = client.open().unwrap(); - assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. })); - - let sent = client.send_probe().unwrap(); - assert_eq!(sent.len(), 1); - - let events = client.recv_once().unwrap(); - assert_eq!(events.len(), 1); - assert!(matches!(events[0], ClientEvent::EchoReply { .. })); - - client.close().unwrap(); -} - #[test] fn backend_received_stats_smoke() { for mode in [ diff --git a/crates/irtt-stats/src/ipdv.rs b/crates/irtt-stats/src/ipdv.rs index cf36fd9..3fa9de6 100644 --- a/crates/irtt-stats/src/ipdv.rs +++ b/crates/irtt-stats/src/ipdv.rs @@ -185,81 +185,6 @@ mod tests { } } - #[test] - fn ipdv_tracker_completes_adjacent_pair() { - let mut tracker = IpdvTracker::new(None); - assert!(tracker.insert(ipdv_sample(0, 10)).is_empty()); - - let pairs = tracker.insert(ipdv_sample(1, 14)); - - assert_eq!( - pairs, - vec![CompletedIpdvPair { - previous_seq: 0, - current_seq: 1, - rtt_ipdv_ns: 4, - send_ipdv_ns: None, - receive_ipdv_ns: None, - }] - ); - } - - #[test] - fn ipdv_tracker_gap_fill_completes_both_adjacent_pairs() { - let mut tracker = IpdvTracker::new(None); - assert!(tracker.insert(ipdv_sample(0, 10)).is_empty()); - assert!(tracker.insert(ipdv_sample(2, 20)).is_empty()); - - let pairs = tracker.insert(ipdv_sample(1, 13)); - - assert_eq!( - pairs, - vec![ - CompletedIpdvPair { - previous_seq: 0, - current_seq: 1, - rtt_ipdv_ns: 3, - send_ipdv_ns: None, - receive_ipdv_ns: None, - }, - CompletedIpdvPair { - previous_seq: 1, - current_seq: 2, - rtt_ipdv_ns: 7, - send_ipdv_ns: None, - receive_ipdv_ns: None, - }, - ] - ); - } - - #[test] - fn ipdv_tracker_completes_wrapped_adjacent_pair() { - let mut tracker = IpdvTracker::new(None); - assert!(tracker.insert(ipdv_sample(u32::MAX, 10)).is_empty()); - - let pairs = tracker.insert(ipdv_sample(0, 14)); - - assert_eq!( - pairs, - vec![CompletedIpdvPair { - previous_seq: u32::MAX, - current_seq: 0, - rtt_ipdv_ns: 4, - send_ipdv_ns: None, - receive_ipdv_ns: None, - }] - ); - } - - #[test] - fn ipdv_tracker_wrap_gap_does_not_complete_pair() { - let mut tracker = IpdvTracker::new(None); - assert!(tracker.insert(ipdv_sample(u32::MAX - 1, 10)).is_empty()); - - assert!(tracker.insert(ipdv_sample(0, 14)).is_empty()); - } - #[test] fn ipdv_tracker_late_wrapped_previous_completes_pair() { let mut tracker = IpdvTracker::new(None); @@ -289,17 +214,25 @@ mod tests { } #[test] - fn ipdv_tracker_bounded_mode_evicts_old_sequence_state() { - let mut tracker = IpdvTracker::new(Some(2)); - assert!(tracker.insert(ipdv_sample(0, 10)).is_empty()); - assert_eq!(tracker.insert(ipdv_sample(1, 14)).len(), 1); + fn ipdv_tracker_bounded_mode_limits_sequence_state() { + let limit = 4; + let mut tracker = IpdvTracker::new(Some(limit)); - let pairs = tracker.insert(ipdv_sample(2, 20)); + // Far more inserts than the limit, so eviction runs repeatedly. Every + // per-sequence collection must stay bounded by the limit rather than + // growing with the number of samples seen, and eviction must not stop + // adjacent sequences from still completing a pair. + for seq in 0..512_u32 { + let pairs = tracker.insert(ipdv_sample(seq, i128::from(seq))); - assert_eq!(pairs.len(), 1); - assert!(!tracker.samples.contains_key(&0)); - assert!(!tracker.completed_pairs.contains(&0)); - assert!(!tracker.completed_pairs.contains(&1)); - assert!(tracker.completed_pairs.contains(&2)); + assert_eq!(pairs.len(), usize::from(seq > 0), "seq {seq}"); + assert!(tracker.samples.len() <= limit, "seq {seq}"); + assert_eq!( + tracker.sample_order.len(), + tracker.samples.len(), + "seq {seq}" + ); + assert!(tracker.completed_pairs.len() <= limit, "seq {seq}"); + } } }