use crate::common;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::PathBuf;
use std::time::Duration;
const READ_TIMEOUT: Duration = Duration::from_secs(5);
fn local_spawn(db: &std::path::Path) -> (std::process::Child, common::ServerAddrs) {
common::ServerBuilder::new()
.arg_path(db)
.with_pgwire()
.spawn()
}
fn unique_tmpdir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let p = std::env::temp_dir().join(format!("spg-e2e-appname-{label}-{nanos}"));
std::fs::create_dir_all(&p).unwrap();
p
}
struct PgMessage {
ty: u8,
body: Vec<u8>,
}
fn read_message(s: &mut TcpStream) -> PgMessage {
let mut header = [0u8; 5];
s.read_exact(&mut header).expect("header");
let ty = header[0];
let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
let body_len = len.saturating_sub(4);
let mut body = vec![0u8; body_len];
if body_len > 0 {
s.read_exact(&mut body).expect("body");
}
PgMessage { ty, body }
}
fn send_startup_full(s: &mut TcpStream, user: &str, extra: &[(&str, &str)]) {
let mut body = Vec::new();
body.extend_from_slice(&196_608_u32.to_be_bytes());
body.extend_from_slice(b"user\0");
body.extend_from_slice(user.as_bytes());
body.push(0);
for (k, v) in extra {
body.extend_from_slice(k.as_bytes());
body.push(0);
body.extend_from_slice(v.as_bytes());
body.push(0);
}
body.push(0); let total = u32::try_from(body.len() + 4).unwrap();
let mut out = Vec::with_capacity(body.len() + 4);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(&body);
s.write_all(&out).unwrap();
}
fn write_msg(buf: &mut Vec<u8>, ty: u8, body: &[u8]) {
buf.push(ty);
let len = (body.len() + 4) as u32;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(body);
}
fn send_query(s: &mut TcpStream, sql: &str) {
let mut body = Vec::with_capacity(sql.len() + 1);
body.extend_from_slice(sql.as_bytes());
body.push(0);
let mut out = Vec::with_capacity(body.len() + 5);
write_msg(&mut out, b'Q', &body);
s.write_all(&out).unwrap();
}
fn read_until_ready(s: &mut TcpStream) -> Vec<PgMessage> {
let mut out = Vec::new();
loop {
let m = read_message(s);
let z = m.ty == b'Z';
out.push(m);
if z {
return out;
}
}
}
fn open_with(addr: &str, user: &str, extra: &[(&str, &str)]) -> TcpStream {
let mut s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(READ_TIMEOUT)).unwrap();
send_startup_full(&mut s, user, extra);
let _ = read_until_ready(&mut s);
s
}
fn datarow_cell(body: &[u8], col_idx: usize) -> Option<String> {
let cells = u16::from_be_bytes([body[0], body[1]]) as usize;
if col_idx >= cells {
return None;
}
let mut p = 2;
for i in 0..cells {
let len = i32::from_be_bytes([body[p], body[p + 1], body[p + 2], body[p + 3]]);
p += 4;
if len < 0 {
if i == col_idx {
return None;
}
continue;
}
let l = len as usize;
if i == col_idx {
return Some(std::str::from_utf8(&body[p..p + l]).unwrap().to_string());
}
p += l;
}
None
}
fn find_row_cell(msgs: &[PgMessage], key_col: usize, key_val: &str, col: usize) -> Option<String> {
for m in msgs.iter().filter(|m| m.ty == b'D') {
if let Some(k) = datarow_cell(&m.body, key_col) {
if k == key_val {
return datarow_cell(&m.body, col);
}
}
}
None
}
#[test]
fn startup_param_application_name_surfaces_in_activity() {
let dir = unique_tmpdir("startup");
let db = dir.join("spg.db");
let (raw, addrs) = local_spawn(&db);
let _child = common::ChildGuard(raw);
let mut s = open_with(
addrs.pgwire.as_ref().unwrap(),
"alice",
&[("application_name", "psql-alice")],
);
send_query(&mut s, "SELECT * FROM spg_stat_activity");
let msgs = read_until_ready(&mut s);
let appname = find_row_cell(&msgs, 1, "alice", 8).expect("alice row + appname");
assert_eq!(appname, "psql-alice");
}
#[test]
fn set_application_name_updates_activity_row() {
let dir = unique_tmpdir("set");
let db = dir.join("spg.db");
let (raw, addrs) = local_spawn(&db);
let _child = common::ChildGuard(raw);
let mut s = open_with(addrs.pgwire.as_ref().unwrap(), "bob", &[]);
send_query(&mut s, "SELECT * FROM spg_stat_activity");
let msgs0 = read_until_ready(&mut s);
let pre = find_row_cell(&msgs0, 1, "bob", 8).expect("bob row");
assert_eq!(
pre, "",
"pre-SET application_name is empty when no startup param"
);
send_query(&mut s, "SET application_name = 'pytest-runner'");
let _ = read_until_ready(&mut s);
send_query(&mut s, "SELECT * FROM spg_stat_activity");
let msgs1 = read_until_ready(&mut s);
let post = find_row_cell(&msgs1, 1, "bob", 8).expect("bob row after SET");
assert_eq!(post, "pytest-runner");
}
#[test]
fn show_application_name_reads_startup_then_set() {
let dir = unique_tmpdir("show");
let db = dir.join("spg.db");
let (raw, addrs) = local_spawn(&db);
let _child = common::ChildGuard(raw);
let mut s = open_with(
addrs.pgwire.as_ref().unwrap(),
"carol",
&[("application_name", "dbeaver")],
);
send_query(&mut s, "SHOW application_name");
let msgs0 = read_until_ready(&mut s);
let v0 = msgs0
.iter()
.filter(|m| m.ty == b'D')
.find_map(|m| datarow_cell(&m.body, 0))
.expect("SHOW DataRow");
assert_eq!(v0, "dbeaver");
send_query(&mut s, "SET application_name = 'reporter'");
let _ = read_until_ready(&mut s);
send_query(&mut s, "SHOW application_name");
let msgs1 = read_until_ready(&mut s);
let v1 = msgs1
.iter()
.filter(|m| m.ty == b'D')
.find_map(|m| datarow_cell(&m.body, 0))
.expect("SHOW DataRow");
assert_eq!(v1, "reporter");
}