summaryrefslogtreecommitdiffstats
path: root/src/bin/psql/t
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/psql/t')
-rw-r--r--src/bin/psql/t/001_basic.pl328
-rw-r--r--src/bin/psql/t/010_tab_completion.pl452
-rw-r--r--src/bin/psql/t/020_cancel.pl80
3 files changed, 860 insertions, 0 deletions
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
new file mode 100644
index 0000000..f447845
--- /dev/null
+++ b/src/bin/psql/t/001_basic.pl
@@ -0,0 +1,328 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('psql');
+program_version_ok('psql');
+program_options_handling_ok('psql');
+
+# Execute a psql command and check its output.
+sub psql_like
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my ($node, $sql, $expected_stdout, $test_name) = @_;
+
+ my ($ret, $stdout, $stderr) = $node->psql('postgres', $sql);
+
+ is($ret, 0, "$test_name: exit code 0");
+ is($stderr, '', "$test_name: no stderr");
+ like($stdout, $expected_stdout, "$test_name: matches");
+
+ return;
+}
+
+# Execute a psql command and check that it fails and check the stderr.
+sub psql_fails_like
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my ($node, $sql, $expected_stderr, $test_name) = @_;
+
+ # Use the context of a WAL sender, some of the tests rely on that.
+ my ($ret, $stdout, $stderr) =
+ $node->psql('postgres', $sql, replication => 'database');
+
+ isnt($ret, 0, "$test_name: exit code not 0");
+ like($stderr, $expected_stderr, "$test_name: matches");
+
+ return;
+}
+
+# test --help=foo, analogous to program_help_ok()
+foreach my $arg (qw(commands variables))
+{
+ my ($stdout, $stderr);
+ my $result;
+
+ $result = IPC::Run::run [ 'psql', "--help=$arg" ], '>', \$stdout, '2>',
+ \$stderr;
+ ok($result, "psql --help=$arg exit code 0");
+ isnt($stdout, '', "psql --help=$arg goes to stdout");
+ is($stderr, '', "psql --help=$arg nothing to stderr");
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(extra => [ '--locale=C', '--encoding=UTF8' ]);
+$node->append_conf(
+ 'postgresql.conf', q{
+wal_level = 'logical'
+max_replication_slots = 4
+max_wal_senders = 4
+});
+$node->start;
+
+psql_like($node, '\copyright', qr/Copyright/, '\copyright');
+psql_like($node, '\help', qr/ALTER/, '\help without arguments');
+psql_like($node, '\help SELECT', qr/SELECT/, '\help with argument');
+
+# Test clean handling of unsupported replication command responses
+psql_fails_like(
+ $node,
+ 'START_REPLICATION 0/0',
+ qr/unexpected PQresultStatus: 8$/,
+ 'handling of unexpected PQresultStatus');
+
+# test \timing
+psql_like(
+ $node,
+ '\timing on
+SELECT 1',
+ qr/^1$
+^Time: \d+[.,]\d\d\d ms/m,
+ '\timing with successful query');
+
+# test \timing with query that fails
+{
+ my ($ret, $stdout, $stderr) =
+ $node->psql('postgres', "\\timing on\nSELECT error");
+ isnt($ret, 0, '\timing with query error: query failed');
+ like(
+ $stdout,
+ qr/^Time: \d+[.,]\d\d\d ms/m,
+ '\timing with query error: timing output appears');
+ unlike(
+ $stdout,
+ qr/^Time: 0[.,]000 ms/m,
+ '\timing with query error: timing was updated');
+}
+
+# test that ENCODING variable is set and that it is updated when
+# client encoding is changed
+psql_like(
+ $node,
+ '\echo :ENCODING
+set client_encoding = LATIN1;
+\echo :ENCODING',
+ qr/^UTF8$
+^LATIN1$/m,
+ 'ENCODING variable is set and updated');
+
+# test LISTEN/NOTIFY
+psql_like(
+ $node,
+ 'LISTEN foo;
+NOTIFY foo;',
+ qr/^Asynchronous notification "foo" received from server process with PID \d+\.$/,
+ 'notification');
+
+psql_like(
+ $node,
+ "LISTEN foo;
+NOTIFY foo, 'bar';",
+ qr/^Asynchronous notification "foo" with payload "bar" received from server process with PID \d+\.$/,
+ 'notification with payload');
+
+# test behavior and output on server crash
+my ($ret, $out, $err) = $node->psql('postgres',
+ "SELECT 'before' AS running;\n"
+ . "SELECT pg_terminate_backend(pg_backend_pid());\n"
+ . "SELECT 'AFTER' AS not_running;\n");
+
+is($ret, 2, 'server crash: psql exit code');
+like($out, qr/before/, 'server crash: output before crash');
+ok($out !~ qr/AFTER/, 'server crash: no output after crash');
+is( $err,
+ 'psql:<stdin>:2: FATAL: terminating connection due to administrator command
+psql:<stdin>:2: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+psql:<stdin>:2: error: connection to server was lost',
+ 'server crash: error message');
+
+# test \errverbose
+#
+# (This is not in the regular regression tests because the output
+# contains the source code location and we don't want to have to
+# update that every time it changes.)
+
+psql_like(
+ $node,
+ 'SELECT 1;
+\errverbose',
+ qr/^1\nThere is no previous error\.$/,
+ '\errverbose with no previous error');
+
+# There are three main ways to run a query that might affect
+# \errverbose: The normal way, using a cursor by setting FETCH_COUNT,
+# and using \gdesc. Test them all.
+
+like(
+ ( $node->psql(
+ 'postgres',
+ "SELECT error;\n\\errverbose",
+ on_error_stop => 0))[2],
+ qr/\A^psql:<stdin>:1: ERROR: .*$
+^LINE 1: SELECT error;$
+^ *^.*$
+^psql:<stdin>:2: error: ERROR: [0-9A-Z]{5}: .*$
+^LINE 1: SELECT error;$
+^ *^.*$
+^LOCATION: .*$/m,
+ '\errverbose after normal query with error');
+
+like(
+ ( $node->psql(
+ 'postgres',
+ "\\set FETCH_COUNT 1\nSELECT error;\n\\errverbose",
+ on_error_stop => 0))[2],
+ qr/\A^psql:<stdin>:2: ERROR: .*$
+^LINE 2: SELECT error;$
+^ *^.*$
+^psql:<stdin>:3: error: ERROR: [0-9A-Z]{5}: .*$
+^LINE 2: SELECT error;$
+^ *^.*$
+^LOCATION: .*$/m,
+ '\errverbose after FETCH_COUNT query with error');
+
+like(
+ ( $node->psql(
+ 'postgres',
+ "SELECT error\\gdesc\n\\errverbose",
+ on_error_stop => 0))[2],
+ qr/\A^psql:<stdin>:1: ERROR: .*$
+^LINE 1: SELECT error$
+^ *^.*$
+^psql:<stdin>:2: error: ERROR: [0-9A-Z]{5}: .*$
+^LINE 1: SELECT error$
+^ *^.*$
+^LOCATION: .*$/m,
+ '\errverbose after \gdesc with error');
+
+# Check behavior when using multiple -c and -f switches.
+# Note that we cannot test backend-side errors as tests are unstable in this
+# case: IPC::Run can complain about a SIGPIPE if psql quits before reading a
+# query result.
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+$node->safe_psql('postgres', "CREATE TABLE tab_psql_single (a int);");
+
+# Tests with ON_ERROR_STOP.
+$node->command_ok(
+ [
+ 'psql', '-X',
+ '--single-transaction', '-v',
+ 'ON_ERROR_STOP=1', '-c',
+ 'INSERT INTO tab_psql_single VALUES (1)', '-c',
+ 'INSERT INTO tab_psql_single VALUES (2)'
+ ],
+ 'ON_ERROR_STOP, --single-transaction and multiple -c switches');
+my $row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '2',
+ '--single-transaction commits transaction, ON_ERROR_STOP and multiple -c switches'
+);
+
+$node->command_fails(
+ [
+ 'psql', '-X',
+ '--single-transaction', '-v',
+ 'ON_ERROR_STOP=1', '-c',
+ 'INSERT INTO tab_psql_single VALUES (3)', '-c',
+ "\\copy tab_psql_single FROM '$tempdir/nonexistent'"
+ ],
+ 'ON_ERROR_STOP, --single-transaction and multiple -c switches, error');
+$row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '2',
+ 'client-side error rolls back transaction, ON_ERROR_STOP and multiple -c switches'
+);
+
+# Tests mixing files and commands.
+my $copy_sql_file = "$tempdir/tab_copy.sql";
+my $insert_sql_file = "$tempdir/tab_insert.sql";
+append_to_file($copy_sql_file,
+ "\\copy tab_psql_single FROM '$tempdir/nonexistent';");
+append_to_file($insert_sql_file, 'INSERT INTO tab_psql_single VALUES (4);');
+$node->command_ok(
+ [
+ 'psql', '-X', '--single-transaction', '-v',
+ 'ON_ERROR_STOP=1', '-f', $insert_sql_file, '-f',
+ $insert_sql_file
+ ],
+ 'ON_ERROR_STOP, --single-transaction and multiple -f switches');
+$row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '4',
+ '--single-transaction commits transaction, ON_ERROR_STOP and multiple -f switches'
+);
+
+$node->command_fails(
+ [
+ 'psql', '-X', '--single-transaction', '-v',
+ 'ON_ERROR_STOP=1', '-f', $insert_sql_file, '-f',
+ $copy_sql_file
+ ],
+ 'ON_ERROR_STOP, --single-transaction and multiple -f switches, error');
+$row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '4',
+ 'client-side error rolls back transaction, ON_ERROR_STOP and multiple -f switches'
+);
+
+# Tests without ON_ERROR_STOP.
+# The last switch fails on \copy. The command returns a failure and the
+# transaction commits.
+$node->command_fails(
+ [
+ 'psql', '-X',
+ '--single-transaction', '-f',
+ $insert_sql_file, '-f',
+ $insert_sql_file, '-c',
+ "\\copy tab_psql_single FROM '$tempdir/nonexistent'"
+ ],
+ 'no ON_ERROR_STOP, --single-transaction and multiple -f/-c switches');
+$row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '6',
+ 'client-side error commits transaction, no ON_ERROR_STOP and multiple -f/-c switches'
+);
+
+# The last switch fails on \copy coming from an input file. The command
+# returns a success and the transaction commits.
+$node->command_ok(
+ [
+ 'psql', '-X', '--single-transaction', '-f',
+ $insert_sql_file, '-f', $insert_sql_file, '-f',
+ $copy_sql_file
+ ],
+ 'no ON_ERROR_STOP, --single-transaction and multiple -f switches');
+$row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '8',
+ 'client-side error commits transaction, no ON_ERROR_STOP and multiple -f switches'
+);
+
+# The last switch makes the command return a success, and the contents of
+# the transaction commit even if there is a failure in-between.
+$node->command_ok(
+ [
+ 'psql', '-X',
+ '--single-transaction', '-c',
+ 'INSERT INTO tab_psql_single VALUES (5)', '-f',
+ $copy_sql_file, '-c',
+ 'INSERT INTO tab_psql_single VALUES (6)'
+ ],
+ 'no ON_ERROR_STOP, --single-transaction and multiple -c switches');
+$row_count =
+ $node->safe_psql('postgres', 'SELECT count(*) FROM tab_psql_single');
+is($row_count, '10',
+ 'client-side error commits transaction, no ON_ERROR_STOP and multiple -c switches'
+);
+
+done_testing();
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
new file mode 100644
index 0000000..2eea515
--- /dev/null
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -0,0 +1,452 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+# Do nothing unless Makefile has told us that the build is --with-readline.
+if (!defined($ENV{with_readline}) || $ENV{with_readline} ne 'yes')
+{
+ plan skip_all => 'readline is not supported by this build';
+}
+
+# Also, skip if user has set environment variable to command that.
+# This is mainly intended to allow working around some of the more broken
+# versions of libedit --- some users might find them acceptable even if
+# they won't pass these tests.
+if (defined($ENV{SKIP_READLINE_TESTS}))
+{
+ plan skip_all => 'SKIP_READLINE_TESTS is set';
+}
+
+# If we don't have IO::Pty, forget it, because IPC::Run depends on that
+# to support pty connections
+eval { require IO::Pty; };
+if ($@)
+{
+ plan skip_all => 'IO::Pty is needed to run this test';
+}
+
+# start a new server
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+# set up a few database objects
+$node->safe_psql('postgres',
+ "CREATE TABLE tab1 (c1 int primary key, c2 text);\n"
+ . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
+ . "CREATE TABLE mytab246 (f1 int, f2 text);\n"
+ . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n"
+ . "CREATE TYPE enum1 AS ENUM ('foo', 'bar', 'baz', 'BLACK');\n"
+ . "CREATE PUBLICATION some_publication;\n");
+
+# Developers would not appreciate this test adding a bunch of junk to
+# their ~/.psql_history, so be sure to redirect history into a temp file.
+# We might as well put it in the test log directory, so that buildfarm runs
+# capture the result for possible debugging purposes.
+my $historyfile = "${PostgreSQL::Test::Utils::log_path}/010_psql_history.txt";
+$ENV{PSQL_HISTORY} = $historyfile;
+
+# Another pitfall for developers is that they might have a ~/.inputrc
+# file that changes readline's behavior enough to affect this test.
+# So ignore any such file.
+$ENV{INPUTRC} = '/dev/null';
+
+# Unset $TERM so that readline/libedit won't use any terminal-dependent
+# escape sequences; that leads to way too many cross-version variations
+# in the output.
+delete $ENV{TERM};
+# Some versions of readline inspect LS_COLORS, so for luck unset that too.
+delete $ENV{LS_COLORS};
+
+# In a VPATH build, we'll be started in the source directory, but we want
+# to run in the build directory so that we can use relative paths to
+# access the tmp_check subdirectory; otherwise the output from filename
+# completion tests is too variable.
+if ($ENV{TESTDIR})
+{
+ chdir $ENV{TESTDIR} or die "could not chdir to \"$ENV{TESTDIR}\": $!";
+}
+
+# Create some junk files for filename completion testing.
+my $FH;
+open $FH, ">", "tmp_check/somefile"
+ or die("could not create file \"tmp_check/somefile\": $!");
+print $FH "some stuff\n";
+close $FH;
+open $FH, ">", "tmp_check/afile123"
+ or die("could not create file \"tmp_check/afile123\": $!");
+print $FH "more stuff\n";
+close $FH;
+open $FH, ">", "tmp_check/afile456"
+ or die("could not create file \"tmp_check/afile456\": $!");
+print $FH "other stuff\n";
+close $FH;
+
+# fire up an interactive psql session
+my $in = '';
+my $out = '';
+
+my $timer = timer($PostgreSQL::Test::Utils::timeout_default);
+
+my $h = $node->interactive_psql('postgres', \$in, \$out, $timer);
+
+like($out, qr/psql/, "print startup banner");
+
+# Simple test case: type something and see if psql responds as expected
+sub check_completion
+{
+ my ($send, $pattern, $annotation) = @_;
+
+ # report test failures from caller location
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ # reset output collector
+ $out = "";
+ # restart per-command timer
+ $timer->start($PostgreSQL::Test::Utils::timeout_default);
+ # send the data to be sent
+ $in .= $send;
+ # wait ...
+ pump $h until ($out =~ $pattern || $timer->is_expired);
+ my $okay = ($out =~ $pattern && !$timer->is_expired);
+ ok($okay, $annotation);
+ # for debugging, log actual output if it didn't match
+ local $Data::Dumper::Terse = 1;
+ local $Data::Dumper::Useqq = 1;
+ diag 'Actual output was ' . Dumper($out) . "Did not match \"$pattern\"\n"
+ if !$okay;
+ return;
+}
+
+# Clear query buffer to start over
+# (won't work if we are inside a string literal!)
+sub clear_query
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ check_completion("\\r\n", qr/Query buffer reset.*postgres=# /s,
+ "\\r works");
+ return;
+}
+
+# Clear current line to start over
+# (this will work in an incomplete string literal, but it's less desirable
+# than clear_query because we lose evidence in the history file)
+sub clear_line
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ check_completion("\025\n", qr/postgres=# /, "control-U works");
+ return;
+}
+
+# check basic command completion: SEL<tab> produces SELECT<space>
+check_completion("SEL\t", qr/SELECT /, "complete SEL<tab> to SELECT");
+
+clear_query();
+
+# check case variation is honored
+check_completion("sel\t", qr/select /, "complete sel<tab> to select");
+
+# check basic table name completion
+check_completion("* from t\t", qr/\* from tab1 /, "complete t<tab> to tab1");
+
+clear_query();
+
+# check table name completion with multiple alternatives
+# note: readline might print a bell before the completion
+check_completion(
+ "select * from my\t",
+ qr/select \* from my\a?tab/,
+ "complete my<tab> to mytab when there are multiple choices");
+
+# some versions of readline/libedit require two tabs here, some only need one
+check_completion(
+ "\t\t",
+ qr/mytab123 +mytab246/,
+ "offer multiple table choices");
+
+check_completion("2\t", qr/246 /,
+ "finish completion of one of multiple table choices");
+
+clear_query();
+
+# check handling of quoted names
+check_completion(
+ "select * from \"my\t",
+ qr/select \* from "my\a?tab/,
+ "complete \"my<tab> to \"mytab when there are multiple choices");
+
+check_completion(
+ "\t\t",
+ qr/"mytab123" +"mytab246"/,
+ "offer multiple quoted table choices");
+
+# note: broken versions of libedit want to backslash the closing quote;
+# not much we can do about that
+check_completion("2\t", qr/246\\?" /,
+ "finish completion of one of multiple quoted table choices");
+
+# note: broken versions of libedit may leave us in a state where psql
+# thinks there's an unclosed double quote, so that we have to use
+# clear_line not clear_query here
+clear_line();
+
+# check handling of mixed-case names
+# note: broken versions of libedit want to backslash the closing quote;
+# not much we can do about that
+check_completion(
+ "select * from \"mi\t",
+ qr/"mixedName\\?" /,
+ "complete a mixed-case name");
+
+# as above, must use clear_line not clear_query here
+clear_line();
+
+# check case folding
+check_completion("select * from TAB\t", qr/tab1 /, "automatically fold case");
+
+clear_query();
+
+# check case-sensitive keyword replacement
+# note: various versions of readline/libedit handle backspacing
+# differently, so just check that the replacement comes out correctly
+check_completion("\\DRD\t", qr/drds /, "complete \\DRD<tab> to \\drds");
+
+# broken versions of libedit require clear_line not clear_query here
+clear_line();
+
+# check completion of a schema-qualified name
+check_completion("select * from pub\t",
+ qr/public\./, "complete schema when relevant");
+
+check_completion("tab\t", qr/tab1 /, "complete schema-qualified name");
+
+clear_query();
+
+check_completion(
+ "select * from PUBLIC.t\t",
+ qr/public\.tab1 /,
+ "automatically fold case in schema-qualified name");
+
+clear_query();
+
+# check interpretation of referenced names
+check_completion(
+ "alter table tab1 drop constraint \t",
+ qr/tab1_pkey /,
+ "complete index name for referenced table");
+
+clear_query();
+
+check_completion(
+ "alter table TAB1 drop constraint \t",
+ qr/tab1_pkey /,
+ "complete index name for referenced table, with downcasing");
+
+clear_query();
+
+check_completion(
+ "alter table public.\"tab1\" drop constraint \t",
+ qr/tab1_pkey /,
+ "complete index name for referenced table, with schema and quoting");
+
+clear_query();
+
+# check variant where we're completing a qualified name from a refname
+# (this one also checks successful completion in a multiline command)
+check_completion(
+ "comment on constraint tab1_pkey \n on public.\t",
+ qr/public\.tab1/,
+ "complete qualified name from object reference");
+
+clear_query();
+
+# check filename completion
+check_completion(
+ "\\lo_import tmp_check/some\t",
+ qr|tmp_check/somefile |,
+ "filename completion with one possibility");
+
+clear_query();
+
+# note: readline might print a bell before the completion
+check_completion(
+ "\\lo_import tmp_check/af\t",
+ qr|tmp_check/af\a?ile|,
+ "filename completion with multiple possibilities");
+
+# broken versions of libedit require clear_line not clear_query here
+clear_line();
+
+# COPY requires quoting
+# note: broken versions of libedit want to backslash the closing quote;
+# not much we can do about that
+check_completion(
+ "COPY foo FROM tmp_check/some\t",
+ qr|'tmp_check/somefile\\?' |,
+ "quoted filename completion with one possibility");
+
+clear_line();
+
+check_completion(
+ "COPY foo FROM tmp_check/af\t",
+ qr|'tmp_check/afile|,
+ "quoted filename completion with multiple possibilities");
+
+# some versions of readline/libedit require two tabs here, some only need one
+# also, some will offer the whole path name and some just the file name
+# the quotes might appear, too
+check_completion(
+ "\t\t",
+ qr|afile123'? +'?(tmp_check/)?afile456|,
+ "offer multiple file choices");
+
+clear_line();
+
+# check enum label completion
+# some versions of readline/libedit require two tabs here, some only need one
+# also, some versions will offer quotes, some will not
+check_completion(
+ "ALTER TYPE enum1 RENAME VALUE 'ba\t\t",
+ qr|'?bar'? +'?baz'?|,
+ "offer multiple enum choices");
+
+clear_line();
+
+# enum labels are case sensitive, so this should complete BLACK immediately
+check_completion(
+ "ALTER TYPE enum1 RENAME VALUE 'B\t",
+ qr|BLACK|,
+ "enum labels are case sensitive");
+
+clear_line();
+
+# check timezone name completion
+check_completion("SET timezone TO am\t",
+ qr|'America/|, "offer partial timezone name");
+
+check_completion("new_\t", qr|New_York|, "complete partial timezone name");
+
+clear_line();
+
+# check completion of a keyword offered in addition to object names;
+# such a keyword should obey COMP_KEYWORD_CASE
+foreach (
+ [ 'lower', 'CO', 'column' ],
+ [ 'upper', 'co', 'COLUMN' ],
+ [ 'preserve-lower', 'co', 'column' ],
+ [ 'preserve-upper', 'CO', 'COLUMN' ],)
+{
+ my ($case, $in, $out) = @$_;
+
+ check_completion(
+ "\\set COMP_KEYWORD_CASE $case\n",
+ qr/postgres=#/,
+ "set completion case to '$case'");
+ check_completion("alter table tab1 rename $in\t\t\t",
+ qr|$out|,
+ "offer keyword $out for input $in<TAB>, COMP_KEYWORD_CASE = $case");
+ clear_query();
+}
+
+# alternate path where keyword comes from SchemaQuery
+check_completion(
+ "DROP TYPE big\t",
+ qr/DROP TYPE bigint /,
+ "offer keyword from SchemaQuery");
+
+clear_query();
+
+# check create_command_generator
+check_completion(
+ "CREATE TY\t",
+ qr/CREATE TYPE /,
+ "check create_command_generator");
+
+clear_query();
+
+# check words_after_create infrastructure
+check_completion(
+ "CREATE TABLE mytab\t\t",
+ qr/mytab123 +mytab246/,
+ "check words_after_create");
+
+clear_query();
+
+# check VersionedQuery infrastructure
+check_completion(
+ "DROP PUBLIC\t \t\t",
+ qr/DROP PUBLICATION\s+some_publication /,
+ "check VersionedQuery");
+
+clear_query();
+
+# hits ends_with() and logic for completing in multi-line queries
+check_completion("analyze (\n\t\t", qr/VERBOSE/,
+ "check ANALYZE (VERBOSE ...");
+
+clear_query();
+
+# check completions for GUCs
+check_completion(
+ "set interval\t\t",
+ qr/intervalstyle TO/,
+ "complete a GUC name");
+check_completion(" iso\t", qr/iso_8601 /, "complete a GUC enum value");
+
+clear_query();
+
+# same, for qualified GUC names
+check_completion(
+ "DO \$\$begin end\$\$ LANGUAGE plpgsql;\n",
+ qr/postgres=# /,
+ "load plpgsql extension");
+
+check_completion("set plpg\t", qr/plpg\a?sql\./,
+ "complete prefix of a GUC name");
+check_completion(
+ "var\t\t",
+ qr/variable_conflict TO/,
+ "complete a qualified GUC name");
+check_completion(" USE_C\t",
+ qr/use_column/, "complete a qualified GUC enum value");
+
+clear_query();
+
+# check completions for psql variables
+check_completion("\\set VERB\t", qr/VERBOSITY /,
+ "complete a psql variable name");
+check_completion("def\t", qr/default /, "complete a psql variable value");
+
+clear_query();
+
+check_completion(
+ "\\echo :VERB\t",
+ qr/:VERBOSITY /,
+ "complete an interpolated psql variable name");
+
+clear_query();
+
+# check no-completions code path
+check_completion("blarg \t\t", qr//, "check completion failure path");
+
+clear_query();
+
+# send psql an explicit \q to shut it down, else pty won't close properly
+$timer->start($PostgreSQL::Test::Utils::timeout_default);
+$in .= "\\q\n";
+finish $h or die "psql returned $?";
+$timer->reset;
+
+# done
+$node->stop;
+done_testing();
diff --git a/src/bin/psql/t/020_cancel.pl b/src/bin/psql/t/020_cancel.pl
new file mode 100644
index 0000000..0265b86
--- /dev/null
+++ b/src/bin/psql/t/020_cancel.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+
+# Test query canceling by sending SIGINT to a running psql
+#
+# There is, as of this writing, no documented way to get the PID of
+# the process from IPC::Run. As a workaround, we have psql print its
+# own PID (which is the parent of the shell launched by psql) to a
+# file.
+if ($windows_os)
+{
+ plan skip_all => "cancel test requires a Unix shell";
+}
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+local %ENV = $node->_get_env();
+
+my ($stdin, $stdout, $stderr);
+
+# Test whether shell supports $PPID. It's part of POSIX, but some
+# pre-/non-POSIX shells don't support it (e.g., NetBSD).
+$stdin = "\\! echo \$PPID";
+IPC::Run::run([ 'psql', '-X', '-v', 'ON_ERROR_STOP=1' ],
+ '<', \$stdin, '>', \$stdout, '2>', \$stderr);
+$stdout =~ /^\d+$/ or skip "shell apparently does not support \$PPID", 2;
+
+# Now start the real test
+my $h = IPC::Run::start([ 'psql', '-X', '-v', 'ON_ERROR_STOP=1' ],
+ \$stdin, \$stdout, \$stderr);
+
+# Get the PID
+$stdout = '';
+$stderr = '';
+$stdin = "\\! echo \$PPID >$tempdir/psql.pid\n";
+pump $h while length $stdin;
+my $count;
+my $psql_pid;
+until (
+ -s "$tempdir/psql.pid"
+ and ($psql_pid =
+ PostgreSQL::Test::Utils::slurp_file("$tempdir/psql.pid")) =~
+ /^\d+\n/s)
+{
+ ($count++ < 100 * $PostgreSQL::Test::Utils::timeout_default)
+ or die "pid file did not appear";
+ usleep(10_000);
+}
+
+# Send sleep command and wait until the server has registered it
+$stdin = "select pg_sleep($PostgreSQL::Test::Utils::timeout_default);\n";
+pump $h while length $stdin;
+$node->poll_query_until('postgres',
+ q{SELECT (SELECT count(*) FROM pg_stat_activity WHERE query ~ '^select pg_sleep') > 0;}
+) or die "timed out";
+
+# Send cancel request
+kill 'INT', $psql_pid;
+
+my $result = finish $h;
+
+ok(!$result, 'query failed as expected');
+like(
+ $stderr,
+ qr/canceling statement due to user request/,
+ 'query was canceled');
+
+done_testing();