use faucet_conformance::{
assert_bookmark_roundtrip, assert_bounded_memory, assert_config_schema_valid_value,
assert_errors_not_panics,
};
use faucet_core::Source;
use faucet_source_postgres_cdc::{PostgresCdcSource, PostgresCdcSourceConfig};
use std::time::Duration;
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
use testcontainers_modules::postgres::Postgres;
use tokio_postgres::NoTls;
const BATCH: usize = 250;
const TOTAL: usize = 600;
#[test]
fn conformance_config_schema_valid() {
let schema = serde_json::to_value(schemars::schema_for!(PostgresCdcSourceConfig)).unwrap();
assert_config_schema_valid_value(&schema, "faucet-source-postgres-cdc");
}
async fn start_postgres() -> (ContainerAsync<Postgres>, String) {
let image = Postgres::default()
.with_host_auth()
.with_tag("16-alpine")
.with_cmd([
"postgres",
"-c",
"wal_level=logical",
"-c",
"max_wal_senders=4",
"-c",
"max_replication_slots=4",
]);
let container: ContainerAsync<Postgres> =
image.start().await.expect("postgres container start");
let port = container
.get_host_port_ipv4(5432)
.await
.expect("postgres port");
let url = format!("postgres://postgres@127.0.0.1:{port}/postgres");
(container, url)
}
async fn ddl(url: &str, sql: &str) {
let (client, conn) = tokio_postgres::connect(url, NoTls).await.expect("connect");
tokio::spawn(async move {
let _ = conn.await;
});
client.batch_execute(sql).await.expect("batch execute");
}
fn cfg(url: &str) -> PostgresCdcSourceConfig {
PostgresCdcSourceConfig {
connection_url: url.into(),
slot_name: "conformance_slot".into(),
publication_name: "conformance_pub".into(),
create_slot_if_missing: true,
slot_type: faucet_source_postgres_cdc::SlotType::Permanent,
tls: faucet_source_postgres_cdc::CdcTls::Disable,
start_lsn: None,
proto_version: 1,
idle_timeout: Duration::from_secs(5),
max_messages: Some(TOTAL),
max_staged_records: None,
status_update_interval: Duration::from_secs(1),
tcp_keepalive: Duration::from_secs(60),
batch_size: BATCH,
slot_acquire_retries: 10,
}
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_bounded_memory() {
let (_pg, url) = start_postgres().await;
ddl(
&url,
"CREATE TABLE public.events (id int4 PRIMARY KEY); \
CREATE PUBLICATION conformance_pub FOR TABLE public.events;",
)
.await;
let source = PostgresCdcSource::new(cfg(&url)).await.expect("source");
let _ = source.fetch_all_incremental().await.expect("warm-up");
let (client, conn) = tokio_postgres::connect(&url, NoTls).await.expect("connect");
tokio::spawn(async move {
let _ = conn.await;
});
for i in 0..TOTAL {
client
.execute("INSERT INTO public.events (id) VALUES ($1)", &[&(i as i32)])
.await
.expect("insert");
}
assert_bounded_memory(&source, BATCH, TOTAL).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_bookmark_roundtrip() {
const N: usize = 300;
let (_pg, url) = start_postgres().await;
ddl(
&url,
"CREATE TABLE public.events (id int4 PRIMARY KEY); \
CREATE PUBLICATION conformance_pub FOR TABLE public.events;",
)
.await;
let mut c = cfg(&url);
c.max_messages = None;
c.idle_timeout = Duration::from_secs(15);
let source = PostgresCdcSource::new(c).await.expect("source");
let _ = source.fetch_all_incremental().await.expect("warm-up");
let (client, conn) = tokio_postgres::connect(&url, NoTls).await.expect("connect");
tokio::spawn(async move {
let _ = conn.await;
});
for i in 0..N {
client
.execute("INSERT INTO public.events (id) VALUES ($1)", &[&(i as i32)])
.await
.expect("insert");
}
assert_bookmark_roundtrip(&source).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_errors_not_panics() {
let source = PostgresCdcSource::new(cfg("postgres://postgres@127.0.0.1:1/postgres"))
.await
.expect("new is lazy — must not connect");
assert_errors_not_panics(&source).await;
}