Add basic TAP tests for psql's tab-completion logic.
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 2 Jan 2020 20:02:21 +0000 (15:02 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 2 Jan 2020 20:02:21 +0000 (15:02 -0500)
Up to now, psql's tab-complete.c has had exactly no regression test
coverage.  This patch is an experimental attempt to add some.

This needs Perl's IO::Pty module, which isn't installed everywhere,
so the test script just skips all tests if that's not present.
There may be other portability gotchas too, so I await buildfarm
results with interest.

So far this just covers a few very basic keyword-completion and
query-driven-completion scenarios, which should be enough to let us
get a feel for whether this is practical at all from a portability
standpoint.  If it is, there's lots more that can be done.

Discussion: https://postgr.es/m/10967.1577562752@sss.pgh.pa.us

configure
configure.in
src/Makefile.global.in
src/bin/psql/.gitignore
src/bin/psql/Makefile
src/bin/psql/t/010_tab_completion.pl [new file with mode: 0644]
src/test/perl/PostgresNode.pm

index 5a1019b48435b44e47a1b5b00de80bf8aad329cd..d2d63f2e55582e10d8ed3efa805fe57d23f3bc02 100755 (executable)
--- a/configure
+++ b/configure
@@ -706,6 +706,7 @@ with_libxml
 XML2_CONFIG
 UUID_EXTRA_OBJS
 with_uuid
+with_readline
 with_systemd
 with_selinux
 with_openssl
@@ -8000,6 +8001,7 @@ $as_echo "$as_me: WARNING: *** Readline does not work on MinGW --- disabling" >&
 fi
 
 
+
 #
 # Prefer libedit
 #
index f0f902198f4971a27f4f4d16b5b5d1a04b8341fb..1599fc514d80c3b0ce082e981487f265cc998a7c 100644 (file)
@@ -875,6 +875,7 @@ if test "$PORTNAME" = "win32"; then
     with_readline=no
   fi
 fi
+AC_SUBST(with_readline)
 
 
 #
index 05b66380e0c0288f55b147cf9ed4745d0ce453b3..5002c4776410444c345e9c6e70f70686b46119ab 100644 (file)
@@ -185,6 +185,7 @@ with_perl   = @with_perl@
 with_python    = @with_python@
 with_tcl       = @with_tcl@
 with_openssl   = @with_openssl@
+with_readline  = @with_readline@
 with_selinux   = @with_selinux@
 with_systemd   = @with_systemd@
 with_gssapi    = @with_gssapi@
index c2862b12d6211ba704cf72d2f521c4d675815fc1..10b6dd3a6bd4d22d15c63079d888f34d75d5ab57 100644 (file)
@@ -1,5 +1,5 @@
 /psqlscanslash.c
 /sql_help.h
 /sql_help.c
-
 /psql
+/tmp_check/
index 8a39092f8ae553e2e03753bd03d3fc5801ccf381..2305d93e39cf713a9e1c6b8574070c96736f412e 100644 (file)
@@ -16,6 +16,9 @@ subdir = src/bin/psql
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+# make this available to TAP test scripts
+export with_readline
+
 REFDOCDIR= $(top_srcdir)/doc/src/sgml/ref
 
 override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) $(CPPFLAGS)
@@ -73,8 +76,15 @@ uninstall:
 
 clean distclean:
        rm -f psql$(X) $(OBJS) lex.backup
+       rm -rf tmp_check
 
 # files removed here are supposed to be in the distribution tarball,
 # so do not clean them in the clean/distclean rules
 maintainer-clean: distclean
        rm -f sql_help.h sql_help.c psqlscanslash.c
+
+check:
+       $(prove_check)
+
+installcheck:
+       $(prove_installcheck)
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
new file mode 100644 (file)
index 0000000..1c7610f
--- /dev/null
@@ -0,0 +1,122 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More;
+use IPC::Run qw(pump finish timer);
+
+if ($ENV{with_readline} ne 'yes')
+{
+       plan skip_all => 'readline is not supported by this build';
+}
+
+# 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 = get_new_node('main');
+$node->init;
+$node->start;
+
+# set up a few database objects
+$node->safe_psql('postgres',
+           "CREATE TABLE tab1 (f1 int, f2 text);\n"
+         . "CREATE TABLE mytab123 (f1 int, f2 text);\n"
+         . "CREATE TABLE mytab246 (f1 int, f2 text);\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 = "${TestLib::log_path}/010_psql_history.txt";
+$ENV{PSQL_HISTORY} = $historyfile;
+
+# fire up an interactive psql session
+my $in  = '';
+my $out = '';
+
+my $timer = timer(5);
+
+my $h = $node->interactive_psql('postgres', \$in, \$out, $timer);
+
+ok($out =~ /psql/, "print startup banner");
+
+# Simple test case: type something and see if psql responds as expected
+sub check_completion
+{
+       my ($send, $pattern, $annotation) = @_;
+
+       # reset output collector
+       $out = "";
+       # restart per-command timer
+       $timer->start(5);
+       # send the data to be sent
+       $in .= $send;
+       # wait ...
+       pump $h until ($out =~ m/$pattern/ || $timer->is_expired);
+       my $okay = ($out =~ m/$pattern/ && !$timer->is_expired);
+       ok($okay, $annotation);
+       # for debugging, log actual output if it didn't match
+       note 'Actual output was "' . $out . "\"\n" if !$okay;
+}
+
+# Clear query buffer to start over
+# (won't work if we are inside a string literal!)
+sub clear_query
+{
+       check_completion("\\r\n", "postgres=# ", "\\r works");
+}
+
+# check basic command completion: SEL<tab> produces SELECT<space>
+check_completion("SEL\t", "SELECT ", "complete SEL<tab> to SELECT");
+
+clear_query();
+
+# check case variation is honored
+check_completion("sel\t", "select ", "complete sel<tab> to select");
+
+# check basic table name completion
+check_completion("* from t\t", "\\* 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",
+       "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", "mytab123 +mytab246",
+       "offer multiple table choices");
+
+check_completion("2\t", "246 ",
+       "finish completion of one of multiple table choices");
+
+clear_query();
+
+# check case-sensitive keyword replacement
+# XXX the output here might vary across readline versions
+check_completion(
+       "\\DRD\t",
+       "\\DRD\b\b\bdrds ",
+       "complete \\DRD<tab> to \\drds");
+
+clear_query();
+
+# send psql an explicit \q to shut it down, else pty won't close properly
+$timer->start(5);
+$in .= "\\q\n";
+finish $h or die "psql returned $?";
+$timer->reset;
+
+# done
+$node->stop;
+done_testing();
index 270bd6c8566b9f431b3ffcc92ede05c49442eb12..2e0cf4a2f3ebf4218d83c364a3ba5244229be250 100644 (file)
@@ -1534,6 +1534,73 @@ sub psql
 
 =pod
 
+=item $node->interactive_psql($dbname, \$stdin, \$stdout, $timer, %params) => harness
+
+Invoke B<psql> on B<$dbname> and return an IPC::Run harness object,
+which the caller may use to send interactive input to B<psql>.
+The process's stdin is sourced from the $stdin scalar reference,
+and its stdout and stderr go to the $stdout scalar reference.
+ptys are used so that psql thinks it's being called interactively.
+
+The specified timer object is attached to the harness, as well.
+It's caller's responsibility to select the timeout length, and to
+restart the timer after each command if the timeout is per-command.
+
+psql is invoked in tuples-only unaligned mode with reading of B<.psqlrc>
+disabled.  That may be overridden by passing extra psql parameters.
+
+Dies on failure to invoke psql, or if psql fails to connect.
+Errors occurring later are the caller's problem.
+
+Be sure to "finish" the harness when done with it.
+
+The only extra parameter currently accepted is
+
+=over
+
+=item extra_params => ['--single-transaction']
+
+If given, it must be an array reference containing additional parameters to B<psql>.
+
+=back
+
+This requires IO::Pty in addition to IPC::Run.
+
+=cut
+
+sub interactive_psql
+{
+       my ($self, $dbname, $stdin, $stdout, $timer, %params) = @_;
+
+       my @psql_params = ('psql', '-XAt', '-d', $self->connstr($dbname));
+
+       push @psql_params, @{ $params{extra_params} }
+         if defined $params{extra_params};
+
+       # Ensure there is no data waiting to be sent:
+       $$stdin = "" if ref($stdin);
+       # IPC::Run would otherwise append to existing contents:
+       $$stdout = "" if ref($stdout);
+
+       my $harness = IPC::Run::start \@psql_params,
+         '<pty<', $stdin, '>pty>', $stdout, $timer;
+
+       # Pump until we see psql's help banner.  This ensures that callers
+       # won't write anything to the pty before it's ready, avoiding an
+       # implementation issue in IPC::Run.  Also, it means that psql
+       # connection failures are caught here, relieving callers of
+       # the need to handle those.  (Right now, we have no particularly
+       # good handling for errors anyway, but that might be added later.)
+       pump $harness
+         until $$stdout =~ /Type "help" for help/ || $timer->is_expired;
+
+       die "psql startup timed out" if $timer->is_expired;
+
+       return $harness;
+}
+
+=pod
+
 =item $node->poll_query_until($dbname, $query [, $expected ])
 
 Run B<$query> repeatedly, until it returns the B<$expected> result