Skip to content

Commit 59df840

Browse files
committed
fix(test): use tokio::process::Command in psql suite
`std::process::Command::output()` is synchronous; in an `#[tokio::test]` (single-threaded runtime by default) it blocks the sole runtime thread. The in-process gateway's accept loop runs on that same thread, so when psql connects to the gateway, no thread is left to schedule the accept — psql hangs waiting for a SYN-ACK, output() hangs waiting for psql. Switched to `tokio::process::Command::output().await`. The await parks the future and lets the runtime keep serving the gateway while psql runs. Side benefit: kept the command-print-before-spawn from the previous debug pass — it shows the exact shell-quoted invocation and is copy-pasteable into a separate terminal for triage. Removed the `#[ignore]` annotations since all 10 tests now pass against the gateway forwarding to a real Trino on SDP (verified end-to-end: SHOW intercept, version() intercept, pg_type catalog stub, BEGIN no-op batch, ILIKE rewrite to GERMANY, cast rewrite, multi-row TPC-H region query, syntax-error error path, plus the SELECT 1 basic round-trip — all green in 0.22s).
1 parent 9904d6e commit 59df840

1 file changed

Lines changed: 71 additions & 43 deletions

File tree

tests/psql_integration_test.rs

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
#![allow(clippy::panic, clippy::unwrap_used)]
2222

2323
use std::net::SocketAddr;
24-
use std::process::{Command, Stdio};
24+
use std::process::Stdio;
25+
26+
use tokio::process::Command;
2527

2628
mod common;
2729
use common::{start_gateway, trino_config};
@@ -36,34 +38,66 @@ struct PsqlOutput {
3638
/// Run `psql -A -t -F$'\t' -c "{sql}"` against the gateway and return the
3739
/// captured stdout/stderr/exit. `-A` (no align), `-t` (tuples-only) and
3840
/// `-F` (deterministic field separator) give parseable output.
39-
fn run_psql(addr: SocketAddr, catalog: &str, sql: &str) -> PsqlOutput {
41+
async fn run_psql(addr: SocketAddr, catalog: &str, sql: &str) -> PsqlOutput {
42+
let port = addr.port().to_string();
43+
let host = addr.ip().to_string();
44+
let args: Vec<&str> = vec![
45+
"--no-psqlrc",
46+
"-h",
47+
&host,
48+
"-p",
49+
&port,
50+
"-U",
51+
"trino",
52+
"-d",
53+
catalog,
54+
"-A", // no aligned output
55+
"-t", // tuples-only (no header / footer)
56+
"-F",
57+
"\t",
58+
"-v",
59+
"ON_ERROR_STOP=1",
60+
"-c",
61+
sql,
62+
];
63+
64+
// Print the command before running so a hung test reveals what's
65+
// actually being invoked. Shell-quote each arg so the line can be
66+
// copy-pasted verbatim.
67+
let quoted = args
68+
.iter()
69+
.map(|a| {
70+
if a.chars()
71+
.all(|c| c.is_ascii_alphanumeric() || "-_=.,/:".contains(c))
72+
{
73+
(*a).to_string()
74+
} else {
75+
format!("'{}'", a.replace('\'', r"'\''"))
76+
}
77+
})
78+
.collect::<Vec<_>>()
79+
.join(" ");
80+
println!("$ PGPASSWORD='' psql {quoted}");
81+
82+
// Async tokio::process::Command, not std::process::Command. The
83+
// sync version's `.output()` blocks the runtime thread; with
84+
// `#[tokio::test]`'s default single-threaded runtime, that
85+
// deadlocks against the in-process gateway's accept loop (which
86+
// needs the same thread to schedule). The async `.output().await`
87+
// parks the future so the runtime can keep serving the gateway
88+
// while psql runs.
4089
let out = Command::new("psql")
41-
.arg("--no-psqlrc") // ignore the user's ~/.psqlrc (noise like `\timing on`)
42-
.arg("-h")
43-
.arg(addr.ip().to_string())
44-
.arg("-p")
45-
.arg(addr.port().to_string())
46-
.arg("-U")
47-
.arg("trino")
48-
.arg("-d")
49-
.arg(catalog)
50-
.arg("-A") // no aligned output
51-
.arg("-t") // tuples-only (no header / footer)
52-
.arg("-F")
53-
.arg("\t")
54-
.arg("-v")
55-
.arg("ON_ERROR_STOP=1")
56-
.arg("-c")
57-
.arg(sql)
90+
.args(&args)
5891
.env("PGPASSWORD", "") // auth is disabled in test_config
5992
// Without explicit `Stdio::null()` for stdin, psql inherits the
60-
// cargo-test process's stdin (a pipe) and *blocks reading from it*
61-
// forever — even with `-c`. Closing stdin makes psql exit as soon
62-
// as the `-c` query finishes.
93+
// test process's stdin and blocks reading from it forever —
94+
// even with `-c`. Closing stdin makes psql exit as soon as the
95+
// `-c` query finishes.
6396
.stdin(Stdio::null())
6497
.stdout(Stdio::piped())
6598
.stderr(Stdio::piped())
6699
.output()
100+
.await
67101
.expect("failed to spawn psql; install postgresql-client");
68102
PsqlOutput {
69103
stdout: String::from_utf8_lossy(&out.stdout).to_string(),
@@ -73,7 +107,9 @@ fn run_psql(addr: SocketAddr, catalog: &str, sql: &str) -> PsqlOutput {
73107
}
74108

75109
fn psql_available() -> bool {
76-
Command::new("psql")
110+
// One-time sync probe at test start; no async needed and no
111+
// deadlock risk (gateway hasn't been started yet).
112+
std::process::Command::new("psql")
77113
.arg("--version")
78114
.stdout(Stdio::null())
79115
.stderr(Stdio::null())
@@ -110,7 +146,6 @@ fn skip_if_no_trino() -> Option<postgresql_trino_gateway::config::Config> {
110146
/// With our gateway pointing at a (running) Trino, psql's startup +
111147
/// simple-query path should round-trip cleanly.
112148
#[tokio::test]
113-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
114149
async fn psql_select_one() {
115150
if skip_if_no_psql() {
116151
return;
@@ -119,7 +154,7 @@ async fn psql_select_one() {
119154
return;
120155
};
121156
let addr = start_gateway(config).await;
122-
let out = run_psql(addr, "tpch", "SELECT 1");
157+
let out = run_psql(addr, "tpch", "SELECT 1").await;
123158
assert!(
124159
out.success,
125160
"psql failed: {}\nstderr: {}",
@@ -132,7 +167,6 @@ async fn psql_select_one() {
132167
/// answers locally without touching Trino. Verifies the intercept's
133168
/// RowDescription + DataRow flow with a real psql.
134169
#[tokio::test]
135-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
136170
async fn psql_show_server_version() {
137171
if skip_if_no_psql() {
138172
return;
@@ -141,15 +175,14 @@ async fn psql_show_server_version() {
141175
return;
142176
};
143177
let addr = start_gateway(config).await;
144-
let out = run_psql(addr, "tpch", "SHOW server_version");
178+
let out = run_psql(addr, "tpch", "SHOW server_version").await;
145179
assert!(out.success, "psql failed: {}", out.stderr);
146180
assert_eq!(out.stdout.trim(), "16.6");
147181
}
148182

149183
/// `SELECT version()` exercises the bare-scalar-SELECT intercept. Same
150184
/// flow as above but through a different intercept branch.
151185
#[tokio::test]
152-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
153186
async fn psql_select_version() {
154187
if skip_if_no_psql() {
155188
return;
@@ -158,7 +191,7 @@ async fn psql_select_version() {
158191
return;
159192
};
160193
let addr = start_gateway(config).await;
161-
let out = run_psql(addr, "tpch", "SELECT version()");
194+
let out = run_psql(addr, "tpch", "SELECT version()").await;
162195
assert!(out.success, "psql failed: {}", out.stderr);
163196
assert!(
164197
out.stdout.contains("PostgreSQL 16.6"),
@@ -171,7 +204,6 @@ async fn psql_select_version() {
171204
/// these are intercepted as no-ops. psql's `-c` runs the whole batch in
172205
/// one simple-query message and expects per-statement CommandComplete.
173206
#[tokio::test]
174-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
175207
async fn psql_transaction_no_op_batch() {
176208
if skip_if_no_psql() {
177209
return;
@@ -180,7 +212,7 @@ async fn psql_transaction_no_op_batch() {
180212
return;
181213
};
182214
let addr = start_gateway(config).await;
183-
let out = run_psql(addr, "tpch", "BEGIN; SELECT 1; COMMIT");
215+
let out = run_psql(addr, "tpch", "BEGIN; SELECT 1; COMMIT").await;
184216
assert!(
185217
out.success,
186218
"psql failed: {}\nstderr: {}",
@@ -200,7 +232,6 @@ async fn psql_transaction_no_op_batch() {
200232
/// intercept that Power BI / Npgsql / pgjdbc rely on for type loading.
201233
/// Real psql receives the prebuilt static response.
202234
#[tokio::test]
203-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
204235
async fn psql_pg_type_intercept() {
205236
if skip_if_no_psql() {
206237
return;
@@ -209,7 +240,7 @@ async fn psql_pg_type_intercept() {
209240
return;
210241
};
211242
let addr = start_gateway(config).await;
212-
let out = run_psql(addr, "tpch", "SELECT typname FROM pg_type LIMIT 1");
243+
let out = run_psql(addr, "tpch", "SELECT typname FROM pg_type LIMIT 1").await;
213244
assert!(out.success, "psql failed: {}", out.stderr);
214245
assert!(
215246
!out.stdout.trim().is_empty(),
@@ -224,7 +255,6 @@ async fn psql_pg_type_intercept() {
224255
/// A real Trino-forwarded SELECT against TPC-H. Confirms the streaming
225256
/// bridge produces wire-correct DataRows for psql.
226257
#[tokio::test]
227-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
228258
async fn psql_trino_tpch_nation_count() {
229259
if skip_if_no_psql() {
230260
return;
@@ -233,7 +263,7 @@ async fn psql_trino_tpch_nation_count() {
233263
return;
234264
};
235265
let addr = start_gateway(config).await;
236-
let out = run_psql(addr, "tpch", "SELECT COUNT(*) FROM tpch.sf1.nation");
266+
let out = run_psql(addr, "tpch", "SELECT COUNT(*) FROM tpch.sf1.nation").await;
237267
assert!(
238268
out.success,
239269
"psql failed: {}\nstderr: {}",
@@ -250,7 +280,6 @@ async fn psql_trino_tpch_nation_count() {
250280
/// SQL rewrite — `ILIKE` becomes `lower() LIKE lower()`. psql sees the
251281
/// rewritten output but the query result is what matters.
252282
#[tokio::test]
253-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
254283
async fn psql_trino_ilike_rewrite() {
255284
if skip_if_no_psql() {
256285
return;
@@ -263,7 +292,8 @@ async fn psql_trino_ilike_rewrite() {
263292
addr,
264293
"tpch",
265294
"SELECT name FROM tpch.sf1.nation WHERE name ILIKE 'gER%'",
266-
);
295+
)
296+
.await;
267297
assert!(
268298
out.success,
269299
"psql failed: {}\nstderr: {}",
@@ -280,7 +310,6 @@ async fn psql_trino_ilike_rewrite() {
280310
/// Multi-row, multi-column SELECT — verifies row separator and field
281311
/// separator handling at the psql side.
282312
#[tokio::test]
283-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
284313
async fn psql_trino_multi_row_multi_col() {
285314
if skip_if_no_psql() {
286315
return;
@@ -293,7 +322,8 @@ async fn psql_trino_multi_row_multi_col() {
293322
addr,
294323
"tpch",
295324
"SELECT regionkey, name FROM tpch.sf1.region ORDER BY regionkey",
296-
);
325+
)
326+
.await;
297327
assert!(
298328
out.success,
299329
"psql failed: {}\nstderr: {}",
@@ -313,7 +343,6 @@ async fn psql_trino_multi_row_multi_col() {
313343
/// rewriter operates on the AST; a real psql client confirms the
314344
/// rewritten SQL still produces a correct wire response.
315345
#[tokio::test]
316-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
317346
async fn psql_trino_cast_rewrite() {
318347
if skip_if_no_psql() {
319348
return;
@@ -322,7 +351,7 @@ async fn psql_trino_cast_rewrite() {
322351
return;
323352
};
324353
let addr = start_gateway(config).await;
325-
let out = run_psql(addr, "tpch", "SELECT (1 + 2)::int4 AS result");
354+
let out = run_psql(addr, "tpch", "SELECT (1 + 2)::int4 AS result").await;
326355
assert!(
327356
out.success,
328357
"psql failed: {}\nstderr: {}",
@@ -335,7 +364,6 @@ async fn psql_trino_cast_rewrite() {
335364
/// forwards as ErrorResponse. psql exits non-zero with the message in
336365
/// stderr. Confirms the error-mapping path doesn't crash psql.
337366
#[tokio::test]
338-
#[ignore = "psql subprocess hangs in test harness — debug in progress"]
339367
async fn psql_trino_syntax_error_surfaces() {
340368
if skip_if_no_psql() {
341369
return;
@@ -344,7 +372,7 @@ async fn psql_trino_syntax_error_surfaces() {
344372
return;
345373
};
346374
let addr = start_gateway(config).await;
347-
let out = run_psql(addr, "tpch", "SELECT * FROM no_such_table_at_all");
375+
let out = run_psql(addr, "tpch", "SELECT * FROM no_such_table_at_all").await;
348376
assert!(
349377
!out.success,
350378
"expected non-zero exit; stdout: {}\nstderr: {}",

0 commit comments

Comments
 (0)