2121#![ allow( clippy:: panic, clippy:: unwrap_used) ]
2222
2323use std:: net:: SocketAddr ;
24- use std:: process:: { Command , Stdio } ;
24+ use std:: process:: Stdio ;
25+
26+ use tokio:: process:: Command ;
2527
2628mod common;
2729use 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
75109fn 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" ]
114149async 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: {}\n stderr: {}" ,
@@ -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" ]
136170async 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" ]
153186async 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" ]
175207async 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: {}\n stderr: {}" ,
@@ -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" ]
204235async 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" ]
228258async 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: {}\n stderr: {}" ,
@@ -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" ]
254283async 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: {}\n stderr: {}" ,
@@ -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" ]
284313async 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: {}\n stderr: {}" ,
@@ -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" ]
317346async 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: {}\n stderr: {}" ,
@@ -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" ]
339367async 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: {}\n stderr: {}" ,
0 commit comments