Files
ferrix/crates/integration-tests/src/lib.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 c218ea4d85 Integration tests: one copy of each helper
The test files had grown their own copies of the same helpers: prefix()
twelve times, established() seven, an UPDATE builder six, a seventeen
field PeerConfig literal in fifteen places, a timeout constant in twenty
one. Each is in crates/integration-tests/src once now: peer_config() and
TestServer::with_peer for a configured peer, Announcement for building
UPDATEs, established(), the waits (wait_until, wait_for, wait_for_routes,
wait_for_sessions), get_json, and TIMEOUT. A new field on PeerConfig is
one line to add, not fifteen.

TestPeer::recv has a deadline, so a test that gets nothing fails instead
of hanging; is_closed, which read one byte and took a KEEPALIVE for an
open connection, is closed_within and closed_unanswered.

Net 788 lines fewer; every test checks what it did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 11:54:49 +02:00

100 lines
3.1 KiB
Rust

pub mod announcement;
pub mod test_cache;
pub mod test_peer;
pub mod test_server;
use std::time::Duration;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_server::address::Endpoint;
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use eyre::{Result, bail};
use serde_json::Value;
/// How long a test waits for something the server does on its own: a
/// route reaching the table, a session coming up, a message arriving.
/// Everything happens on the loopback, so it is generous.
pub const TIMEOUT: Duration = Duration::from_secs(5);
/// The policy a peer has with nothing configured, but for one thing: the
/// peers of these tests are all at 127.0.0.1, which no route can have as
/// its next hop, so their routes name another.
pub fn default_policy() -> bgp_server::policy::ImportPolicy {
bgp_server::policy::ImportPolicy {
accept_third_party_next_hop: true,
..Default::default()
}
}
/// A prefix written as text. The tests' prefixes are literals, so a bad
/// one is a mistake in the test.
pub fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// A configured peer with nothing said about it but where it is and what
/// AS it is in: the default hold time, every family, the default policy of
/// these tests, and no role, limits, groups or ADD-PATH. A test says what
/// else with struct update syntax: `PeerConfig { role: Some(..),
/// ..peer_config(..) }`.
pub fn peer_config(name: &str, endpoint: impl Into<Endpoint>, remote_asn: u32) -> PeerConfig {
PeerConfig {
name: name.into(),
endpoint: endpoint.into(),
remote_asn,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
import: default_policy(),
role: None,
require_role: false,
export: Default::default(),
export_limits: None,
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
passive: false,
}
}
/// Waits until `done` holds, looking every few milliseconds, or fails
/// with `what` once [`TIMEOUT`] has passed.
pub async fn wait_until(what: &str, mut done: impl FnMut() -> bool) -> Result<()> {
let waited = tokio::time::timeout(TIMEOUT, async {
while !done() {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await;
if waited.is_err() {
bail!("timed out waiting until {what}");
}
Ok(())
}
/// Waits until `get` has something, and returns it; fails with `what`
/// once [`TIMEOUT`] has passed.
pub async fn wait_for<T>(what: &str, mut get: impl FnMut() -> Option<T>) -> Result<T> {
let mut got = None;
wait_until(what, || {
got = get();
got.is_some()
})
.await?;
Ok(got.expect("the wait ended with something"))
}
/// `GET`s `url` and reads the JSON it answers with. An error status is an
/// error.
pub async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
Ok(client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?)
}