summaryrefslogtreecommitdiffstats
path: root/test/features
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:05:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:05:31 +0000
commite2e51038f71bb0ee8062603e3247d6660a75644b (patch)
tree8cef3c436dc2a3c6301c5b61bc5d8f1362ee918e /test/features
parentInitial commit. (diff)
downloadmycli-e2e51038f71bb0ee8062603e3247d6660a75644b.tar.xz
mycli-e2e51038f71bb0ee8062603e3247d6660a75644b.zip
Adding upstream version 1.27.0.upstream/1.27.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/features')
-rw-r--r--test/features/__init__.py0
-rw-r--r--test/features/auto_vertical.feature12
-rw-r--r--test/features/basic_commands.feature19
-rw-r--r--test/features/connection.feature35
-rw-r--r--test/features/crud_database.feature30
-rw-r--r--test/features/crud_table.feature49
-rw-r--r--test/features/db_utils.py93
-rw-r--r--test/features/environment.py176
-rw-r--r--test/features/fixture_data/help.txt24
-rw-r--r--test/features/fixture_data/help_commands.txt31
-rw-r--r--test/features/fixture_utils.py29
-rw-r--r--test/features/iocommands.feature47
-rw-r--r--test/features/named_queries.feature24
-rw-r--r--test/features/specials.feature7
-rw-r--r--test/features/steps/__init__.py0
-rw-r--r--test/features/steps/auto_vertical.py46
-rw-r--r--test/features/steps/basic_commands.py100
-rw-r--r--test/features/steps/connection.py71
-rw-r--r--test/features/steps/crud_database.py115
-rw-r--r--test/features/steps/crud_table.py112
-rw-r--r--test/features/steps/iocommands.py105
-rw-r--r--test/features/steps/named_queries.py90
-rw-r--r--test/features/steps/specials.py27
-rw-r--r--test/features/steps/utils.py12
-rw-r--r--test/features/steps/wrappers.py117
-rwxr-xr-xtest/features/wrappager.py16
26 files changed, 1387 insertions, 0 deletions
diff --git a/test/features/__init__.py b/test/features/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/features/__init__.py
diff --git a/test/features/auto_vertical.feature b/test/features/auto_vertical.feature
new file mode 100644
index 0000000..aa95718
--- /dev/null
+++ b/test/features/auto_vertical.feature
@@ -0,0 +1,12 @@
+Feature: auto_vertical mode:
+ on, off
+
+ Scenario: auto_vertical on with small query
+ When we run dbcli with --auto-vertical-output
+ and we execute a small query
+ then we see small results in horizontal format
+
+ Scenario: auto_vertical on with large query
+ When we run dbcli with --auto-vertical-output
+ and we execute a large query
+ then we see large results in vertical format
diff --git a/test/features/basic_commands.feature b/test/features/basic_commands.feature
new file mode 100644
index 0000000..a12e899
--- /dev/null
+++ b/test/features/basic_commands.feature
@@ -0,0 +1,19 @@
+Feature: run the cli,
+ call the help command,
+ exit the cli
+
+ Scenario: run "\?" command
+ When we send "\?" command
+ then we see help output
+
+ Scenario: run source command
+ When we send source command
+ then we see help output
+
+ Scenario: check our application_name
+ When we run query to check application_name
+ then we see found
+
+ Scenario: run the cli and exit
+ When we send "ctrl + d"
+ then dbcli exits
diff --git a/test/features/connection.feature b/test/features/connection.feature
new file mode 100644
index 0000000..b06935e
--- /dev/null
+++ b/test/features/connection.feature
@@ -0,0 +1,35 @@
+Feature: connect to a database:
+
+ @requires_local_db
+ Scenario: run mycli on localhost without port
+ When we run mycli with arguments "host=localhost" without arguments "port"
+ When we query "status"
+ Then status contains "via UNIX socket"
+
+ Scenario: run mycli on TCP host without port
+ When we run mycli without arguments "port"
+ When we query "status"
+ Then status contains "via TCP/IP"
+
+ Scenario: run mycli with port but without host
+ When we run mycli without arguments "host"
+ When we query "status"
+ Then status contains "via TCP/IP"
+
+ @requires_local_db
+ Scenario: run mycli without host and port
+ When we run mycli without arguments "host port"
+ When we query "status"
+ Then status contains "via UNIX socket"
+
+ Scenario: run mycli with my.cnf configuration
+ When we create my.cnf file
+ When we run mycli without arguments "host port user pass defaults_file"
+ Then we are logged in
+
+ Scenario: run mycli with mylogin.cnf configuration
+ When we create mylogin.cnf file
+ When we run mycli with arguments "login_path=test_login_path" without arguments "host port user pass defaults_file"
+ Then we are logged in
+
+
diff --git a/test/features/crud_database.feature b/test/features/crud_database.feature
new file mode 100644
index 0000000..f4a7a7f
--- /dev/null
+++ b/test/features/crud_database.feature
@@ -0,0 +1,30 @@
+Feature: manipulate databases:
+ create, drop, connect, disconnect
+
+ Scenario: create and drop temporary database
+ When we create database
+ then we see database created
+ when we drop database
+ then we confirm the destructive warning
+ then we see database dropped
+ when we connect to dbserver
+ then we see database connected
+
+ Scenario: connect and disconnect from test database
+ When we connect to test database
+ then we see database connected
+ when we connect to dbserver
+ then we see database connected
+
+ Scenario: connect and disconnect from quoted test database
+ When we connect to quoted test database
+ then we see database connected
+
+ Scenario: create and drop default database
+ When we create database
+ then we see database created
+ when we connect to tmp database
+ then we see database connected
+ when we drop database
+ then we confirm the destructive warning
+ then we see database dropped and no default database
diff --git a/test/features/crud_table.feature b/test/features/crud_table.feature
new file mode 100644
index 0000000..3384efd
--- /dev/null
+++ b/test/features/crud_table.feature
@@ -0,0 +1,49 @@
+Feature: manipulate tables:
+ create, insert, update, select, delete from, drop
+
+ Scenario: create, insert, select from, update, drop table
+ When we connect to test database
+ then we see database connected
+ when we create table
+ then we see table created
+ when we insert into table
+ then we see record inserted
+ when we update table
+ then we see record updated
+ when we select from table
+ then we see data selected
+ when we delete from table
+ then we confirm the destructive warning
+ then we see record deleted
+ when we drop table
+ then we confirm the destructive warning
+ then we see table dropped
+ when we connect to dbserver
+ then we see database connected
+
+ Scenario: select null values
+ When we connect to test database
+ then we see database connected
+ when we select null
+ then we see null selected
+
+ Scenario: confirm destructive query
+ When we query "create table foo(x integer);"
+ and we query "delete from foo;"
+ and we answer the destructive warning with "y"
+ then we see text "Your call!"
+
+ Scenario: decline destructive query
+ When we query "delete from foo;"
+ and we answer the destructive warning with "n"
+ then we see text "Wise choice!"
+
+ Scenario: no destructive warning if disabled in config
+ When we run dbcli with --no-warn
+ and we query "create table blabla(x integer);"
+ and we query "delete from blabla;"
+ Then we see text "Query OK"
+
+ Scenario: confirm destructive query with invalid response
+ When we query "delete from foo;"
+ then we answer the destructive warning with invalid "1" and see text "is not a valid boolean"
diff --git a/test/features/db_utils.py b/test/features/db_utils.py
new file mode 100644
index 0000000..be550e9
--- /dev/null
+++ b/test/features/db_utils.py
@@ -0,0 +1,93 @@
+import pymysql
+
+
+def create_db(hostname='localhost', port=3306, username=None,
+ password=None, dbname=None):
+ """Create test database.
+
+ :param hostname: string
+ :param port: int
+ :param username: string
+ :param password: string
+ :param dbname: string
+ :return:
+
+ """
+ cn = pymysql.connect(
+ host=hostname,
+ port=port,
+ user=username,
+ password=password,
+ charset='utf8mb4',
+ cursorclass=pymysql.cursors.DictCursor
+ )
+
+ with cn.cursor() as cr:
+ cr.execute('drop database if exists ' + dbname)
+ cr.execute('create database ' + dbname)
+
+ cn.close()
+
+ cn = create_cn(hostname, port, password, username, dbname)
+ return cn
+
+
+def create_cn(hostname, port, password, username, dbname):
+ """Open connection to database.
+
+ :param hostname:
+ :param port:
+ :param password:
+ :param username:
+ :param dbname: string
+ :return: psycopg2.connection
+
+ """
+ cn = pymysql.connect(
+ host=hostname,
+ port=port,
+ user=username,
+ password=password,
+ db=dbname,
+ charset='utf8mb4',
+ cursorclass=pymysql.cursors.DictCursor
+ )
+
+ return cn
+
+
+def drop_db(hostname='localhost', port=3306, username=None,
+ password=None, dbname=None):
+ """Drop database.
+
+ :param hostname: string
+ :param port: int
+ :param username: string
+ :param password: string
+ :param dbname: string
+
+ """
+ cn = pymysql.connect(
+ host=hostname,
+ port=port,
+ user=username,
+ password=password,
+ db=dbname,
+ charset='utf8mb4',
+ cursorclass=pymysql.cursors.DictCursor
+ )
+
+ with cn.cursor() as cr:
+ cr.execute('drop database if exists ' + dbname)
+
+ close_cn(cn)
+
+
+def close_cn(cn=None):
+ """Close connection.
+
+ :param connection: pymysql.connection
+
+ """
+ if cn:
+ cn.close()
diff --git a/test/features/environment.py b/test/features/environment.py
new file mode 100644
index 0000000..1ea0f08
--- /dev/null
+++ b/test/features/environment.py
@@ -0,0 +1,176 @@
+import os
+import shutil
+import sys
+from tempfile import mkstemp
+
+import db_utils as dbutils
+import fixture_utils as fixutils
+import pexpect
+
+from steps.wrappers import run_cli, wait_prompt
+
+test_log_file = os.path.join(os.environ['HOME'], '.mycli.test.log')
+
+
+SELF_CONNECTING_FEATURES = (
+ 'test/features/connection.feature',
+)
+
+
+MY_CNF_PATH = os.path.expanduser('~/.my.cnf')
+MY_CNF_BACKUP_PATH = f'{MY_CNF_PATH}.backup'
+MYLOGIN_CNF_PATH = os.path.expanduser('~/.mylogin.cnf')
+MYLOGIN_CNF_BACKUP_PATH = f'{MYLOGIN_CNF_PATH}.backup'
+
+
+def get_db_name_from_context(context):
+ return context.config.userdata.get(
+ 'my_test_db', None
+ ) or "mycli_behave_tests"
+
+
+
+def before_all(context):
+ """Set env parameters."""
+ os.environ['LINES'] = "100"
+ os.environ['COLUMNS'] = "100"
+ os.environ['EDITOR'] = 'ex'
+ os.environ['LC_ALL'] = 'en_US.UTF-8'
+ os.environ['PROMPT_TOOLKIT_NO_CPR'] = '1'
+ os.environ['MYCLI_HISTFILE'] = os.devnull
+
+ test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
+ login_path_file = os.path.join(test_dir, 'mylogin.cnf')
+# os.environ['MYSQL_TEST_LOGIN_FILE'] = login_path_file
+
+ context.package_root = os.path.abspath(
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+
+ os.environ["COVERAGE_PROCESS_START"] = os.path.join(context.package_root,
+ '.coveragerc')
+
+ context.exit_sent = False
+
+ vi = '_'.join([str(x) for x in sys.version_info[:3]])
+ db_name = get_db_name_from_context(context)
+ db_name_full = '{0}_{1}'.format(db_name, vi)
+
+ # Store get params from config/environment variables
+ context.conf = {
+ 'host': context.config.userdata.get(
+ 'my_test_host',
+ os.getenv('PYTEST_HOST', 'localhost')
+ ),
+ 'port': context.config.userdata.get(
+ 'my_test_port',
+ int(os.getenv('PYTEST_PORT', '3306'))
+ ),
+ 'user': context.config.userdata.get(
+ 'my_test_user',
+ os.getenv('PYTEST_USER', 'root')
+ ),
+ 'pass': context.config.userdata.get(
+ 'my_test_pass',
+ os.getenv('PYTEST_PASSWORD', None)
+ ),
+ 'cli_command': context.config.userdata.get(
+ 'my_cli_command', None) or
+ sys.executable + ' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"',
+ 'dbname': db_name,
+ 'dbname_tmp': db_name_full + '_tmp',
+ 'vi': vi,
+ 'pager_boundary': '---boundary---',
+ }
+
+ _, my_cnf = mkstemp()
+ with open(my_cnf, 'w') as f:
+ f.write(
+ '[client]\n'
+ 'pager={0} {1} {2}\n'.format(
+ sys.executable, os.path.join(context.package_root,
+ 'test/features/wrappager.py'),
+ context.conf['pager_boundary'])
+ )
+ context.conf['defaults-file'] = my_cnf
+ context.conf['myclirc'] = os.path.join(context.package_root, 'test',
+ 'myclirc')
+
+ context.cn = dbutils.create_db(context.conf['host'], context.conf['port'],
+ context.conf['user'],
+ context.conf['pass'],
+ context.conf['dbname'])
+
+ context.fixture_data = fixutils.read_fixture_files()
+
+
+def after_all(context):
+ """Unset env parameters."""
+ dbutils.close_cn(context.cn)
+ dbutils.drop_db(context.conf['host'], context.conf['port'],
+ context.conf['user'], context.conf['pass'],
+ context.conf['dbname'])
+
+ # Restore env vars.
+ #for k, v in context.pgenv.items():
+ # if k in os.environ and v is None:
+ # del os.environ[k]
+ # elif v:
+ # os.environ[k] = v
+
+
+def before_step(context, _):
+ context.atprompt = False
+
+
+def before_scenario(context, arg):
+ with open(test_log_file, 'w') as f:
+ f.write('')
+ if arg.location.filename not in SELF_CONNECTING_FEATURES:
+ run_cli(context)
+ wait_prompt(context)
+
+ if os.path.exists(MY_CNF_PATH):
+ shutil.move(MY_CNF_PATH, MY_CNF_BACKUP_PATH)
+
+ if os.path.exists(MYLOGIN_CNF_PATH):
+ shutil.move(MYLOGIN_CNF_PATH, MYLOGIN_CNF_BACKUP_PATH)
+
+
+def after_scenario(context, _):
+ """Cleans up after each test complete."""
+ with open(test_log_file) as f:
+ for line in f:
+ if 'error' in line.lower():
+ raise RuntimeError(f'Error in log file: {line}')
+
+ if hasattr(context, 'cli') and not context.exit_sent:
+ # Quit nicely.
+ if not context.atprompt:
+ user = context.conf['user']
+ host = context.conf['host']
+ dbname = context.currentdb
+ context.cli.expect_exact(
+ '{0}@{1}:{2}>'.format(
+ user, host, dbname
+ ),
+ timeout=5
+ )
+ context.cli.sendcontrol('c')
+ context.cli.sendcontrol('d')
+ context.cli.expect_exact(pexpect.EOF, timeout=5)
+
+ if os.path.exists(MY_CNF_BACKUP_PATH):
+ shutil.move(MY_CNF_BACKUP_PATH, MY_CNF_PATH)
+
+ if os.path.exists(MYLOGIN_CNF_BACKUP_PATH):
+ shutil.move(MYLOGIN_CNF_BACKUP_PATH, MYLOGIN_CNF_PATH)
+ elif os.path.exists(MYLOGIN_CNF_PATH):
+ # This file was moved in `before_scenario`.
+ # If it exists now, it has been created during a test
+ os.remove(MYLOGIN_CNF_PATH)
+
+
+# TODO: uncomment to debug a failure
+# def after_step(context, step):
+# if step.status == "failed":
+# import ipdb; ipdb.set_trace()
diff --git a/test/features/fixture_data/help.txt b/test/features/fixture_data/help.txt
new file mode 100644
index 0000000..deb499a
--- /dev/null
+++ b/test/features/fixture_data/help.txt
@@ -0,0 +1,24 @@
++--------------------------+-----------------------------------------------+
+| Command | Description |
+|--------------------------+-----------------------------------------------|
+| \# | Refresh auto-completions. |
+| \? | Show Help. |
+| \c[onnect] database_name | Change to a new database. |
+| \d [pattern] | List or describe tables, views and sequences. |
+| \dT[S+] [pattern] | List data types |
+| \df[+] [pattern] | List functions. |
+| \di[+] [pattern] | List indexes. |
+| \dn[+] [pattern] | List schemas. |
+| \ds[+] [pattern] | List sequences. |
+| \dt[+] [pattern] | List tables. |
+| \du[+] [pattern] | List roles. |
+| \dv[+] [pattern] | List views. |
+| \e [file] | Edit the query with external editor. |
+| \l | List databases. |
+| \n[+] [name] | List or execute named queries. |
+| \nd [name [query]] | Delete a named query. |
+| \ns name query | Save a named query. |
+| \refresh | Refresh auto-completions. |
+| \timing | Toggle timing of commands. |
+| \x | Toggle expanded output. |
++--------------------------+-----------------------------------------------+
diff --git a/test/features/fixture_data/help_commands.txt b/test/features/fixture_data/help_commands.txt
new file mode 100644
index 0000000..2c06d5d
--- /dev/null
+++ b/test/features/fixture_data/help_commands.txt
@@ -0,0 +1,31 @@
++-------------+----------------------------+------------------------------------------------------------+
+| Command | Shortcut | Description |
++-------------+----------------------------+------------------------------------------------------------+
+| \G | \G | Display current query results vertically. |
+| \clip | \clip | Copy query to the system clipboard. |
+| \dt | \dt[+] [table] | List or describe tables. |
+| \e | \e | Edit command with editor (uses $EDITOR). |
+| \f | \f [name [args..]] | List or execute favorite queries. |
+| \fd | \fd [name] | Delete a favorite query. |
+| \fs | \fs name query | Save a favorite query. |
+| \l | \l | List databases. |
+| \once | \o [-o] filename | Append next result to an output file (overwrite using -o). |
+| \pipe_once | \| command | Send next result to a subprocess. |
+| \timing | \t | Toggle timing of commands. |
+| connect | \r | Reconnect to the database. Optional database argument. |
+| exit | \q | Exit. |
+| help | \? | Show this help. |
+| nopager | \n | Disable pager, print to stdout. |
+| notee | notee | Stop writing results to an output file. |
+| pager | \P [command] | Set PAGER. Print the query results via PAGER. |
+| prompt | \R | Change prompt format. |
+| quit | \q | Quit. |
+| rehash | \# | Refresh auto-completions. |
+| source | \. filename | Execute commands from file. |
+| status | \s | Get status information from the server. |
+| system | system [command] | Execute a system shell commmand. |
+| tableformat | \T | Change the table format used to output results. |
+| tee | tee [-o] filename | Append all results to an output file (overwrite using -o). |
+| use | \u | Change to a new database. |
+| watch | watch [seconds] [-c] query | Executes the query every [seconds] seconds (by default 5). |
++-------------+----------------------------+------------------------------------------------------------+
diff --git a/test/features/fixture_utils.py b/test/features/fixture_utils.py
new file mode 100644
index 0000000..f85e0f6
--- /dev/null
+++ b/test/features/fixture_utils.py
@@ -0,0 +1,29 @@
+import os
+import io
+
+
+def read_fixture_lines(filename):
+ """Read lines of text from file.
+
+ :param filename: string name
+ :return: list of strings
+
+ """
+ lines = []
+ for line in open(filename):
+ lines.append(line.strip())
+ return lines
+
+
+def read_fixture_files():
+ """Read all files inside fixture_data directory."""
+ fixture_dict = {}
+
+ current_dir = os.path.dirname(__file__)
+ fixture_dir = os.path.join(current_dir, 'fixture_data/')
+ for filename in os.listdir(fixture_dir):
+ if filename not in ['.', '..']:
+ fullname = os.path.join(fixture_dir, filename)
+ fixture_dict[filename] = read_fixture_lines(fullname)
+
+ return fixture_dict
diff --git a/test/features/iocommands.feature b/test/features/iocommands.feature
new file mode 100644
index 0000000..95366eb
--- /dev/null
+++ b/test/features/iocommands.feature
@@ -0,0 +1,47 @@
+Feature: I/O commands
+
+ Scenario: edit sql in file with external editor
+ When we start external editor providing a file name
+ and we type "select * from abc" in the editor
+ and we exit the editor
+ then we see dbcli prompt
+ and we see "select * from abc" in prompt
+
+ Scenario: tee output from query
+ When we tee output
+ and we wait for prompt
+ and we select "select 123456"
+ and we wait for prompt
+ and we notee output
+ and we wait for prompt
+ then we see 123456 in tee output
+
+ Scenario: set delimiter
+ When we query "delimiter $"
+ then delimiter is set to "$"
+
+ Scenario: set delimiter twice
+ When we query "delimiter $"
+ and we query "delimiter ]]"
+ then delimiter is set to "]]"
+
+ Scenario: set delimiter and query on same line
+ When we query "select 123; delimiter $ select 456 $ delimiter %"
+ then we see result "123"
+ and we see result "456"
+ and delimiter is set to "%"
+
+ Scenario: send output to file
+ When we query "\o /tmp/output1.sql"
+ and we query "select 123"
+ and we query "system cat /tmp/output1.sql"
+ then we see result "123"
+
+ Scenario: send output to file two times
+ When we query "\o /tmp/output1.sql"
+ and we query "select 123"
+ and we query "\o /tmp/output2.sql"
+ and we query "select 456"
+ and we query "system cat /tmp/output2.sql"
+ then we see result "456"
+ \ No newline at end of file
diff --git a/test/features/named_queries.feature b/test/features/named_queries.feature
new file mode 100644
index 0000000..5e681ec
--- /dev/null
+++ b/test/features/named_queries.feature
@@ -0,0 +1,24 @@
+Feature: named queries:
+ save, use and delete named queries
+
+ Scenario: save, use and delete named queries
+ When we connect to test database
+ then we see database connected
+ when we save a named query
+ then we see the named query saved
+ when we use a named query
+ then we see the named query executed
+ when we delete a named query
+ then we see the named query deleted
+
+ Scenario: save, use and delete named queries with parameters
+ When we connect to test database
+ then we see database connected
+ when we save a named query with parameters
+ then we see the named query saved
+ when we use named query with parameters
+ then we see the named query with parameters executed
+ when we use named query with too few parameters
+ then we see the named query with parameters fail with missing parameters
+ when we use named query with too many parameters
+ then we see the named query with parameters fail with extra parameters
diff --git a/test/features/specials.feature b/test/features/specials.feature
new file mode 100644
index 0000000..bb36757
--- /dev/null
+++ b/test/features/specials.feature
@@ -0,0 +1,7 @@
+Feature: Special commands
+
+ @wip
+ Scenario: run refresh command
+ When we refresh completions
+ and we wait for prompt
+ then we see completions refresh started
diff --git a/test/features/steps/__init__.py b/test/features/steps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/features/steps/__init__.py
diff --git a/test/features/steps/auto_vertical.py b/test/features/steps/auto_vertical.py
new file mode 100644
index 0000000..e1cb26f
--- /dev/null
+++ b/test/features/steps/auto_vertical.py
@@ -0,0 +1,46 @@
+from textwrap import dedent
+
+from behave import then, when
+
+import wrappers
+from utils import parse_cli_args_to_dict
+
+
+@when('we run dbcli with {arg}')
+def step_run_cli_with_arg(context, arg):
+ wrappers.run_cli(context, run_args=parse_cli_args_to_dict(arg))
+
+
+@when('we execute a small query')
+def step_execute_small_query(context):
+ context.cli.sendline('select 1')
+
+
+@when('we execute a large query')
+def step_execute_large_query(context):
+ context.cli.sendline(
+ 'select {}'.format(','.join([str(n) for n in range(1, 50)])))
+
+
+@then('we see small results in horizontal format')
+def step_see_small_results(context):
+ wrappers.expect_pager(context, dedent("""\
+ +---+\r
+ | 1 |\r
+ +---+\r
+ | 1 |\r
+ +---+\r
+ \r
+ """), timeout=5)
+ wrappers.expect_exact(context, '1 row in set', timeout=2)
+
+
+@then('we see large results in vertical format')
+def step_see_large_results(context):
+ rows = ['{n:3}| {n}'.format(n=str(n)) for n in range(1, 50)]
+ expected = ('***************************[ 1. row ]'
+ '***************************\r\n' +
+ '{}\r\n'.format('\r\n'.join(rows) + '\r\n'))
+
+ wrappers.expect_pager(context, expected, timeout=10)
+ wrappers.expect_exact(context, '1 row in set', timeout=2)
diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py
new file mode 100644
index 0000000..425ef67
--- /dev/null
+++ b/test/features/steps/basic_commands.py
@@ -0,0 +1,100 @@
+"""Steps for behavioral style tests are defined in this module.
+
+Each step is defined by the string decorating it. This string is used
+to call the step in "*.feature" file.
+
+"""
+
+from behave import when
+from textwrap import dedent
+import tempfile
+import wrappers
+
+
+@when('we run dbcli')
+def step_run_cli(context):
+ wrappers.run_cli(context)
+
+
+@when('we wait for prompt')
+def step_wait_prompt(context):
+ wrappers.wait_prompt(context)
+
+
+@when('we send "ctrl + d"')
+def step_ctrl_d(context):
+ """Send Ctrl + D to hopefully exit."""
+ context.cli.sendcontrol('d')
+ context.exit_sent = True
+
+
+@when('we send "\?" command')
+def step_send_help(context):
+ """Send \?
+
+ to see help.
+
+ """
+ context.cli.sendline('\\?')
+ wrappers.expect_exact(
+ context, context.conf['pager_boundary'] + '\r\n', timeout=5)
+
+
+@when(u'we send source command')
+def step_send_source_command(context):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(b'\?')
+ f.flush()
+ context.cli.sendline('\. {0}'.format(f.name))
+ wrappers.expect_exact(
+ context, context.conf['pager_boundary'] + '\r\n', timeout=5)
+
+
+@when(u'we run query to check application_name')
+def step_check_application_name(context):
+ context.cli.sendline(
+ "SELECT 'found' FROM performance_schema.session_connect_attrs WHERE attr_name = 'program_name' AND attr_value = 'mycli'"
+ )
+
+
+@then(u'we see found')
+def step_see_found(context):
+ wrappers.expect_exact(
+ context,
+ context.conf['pager_boundary'] + '\r' + dedent('''
+ +-------+\r
+ | found |\r
+ +-------+\r
+ | found |\r
+ +-------+\r
+ \r
+ ''') + context.conf['pager_boundary'],
+ timeout=5
+ )
+
+
+@then(u'we confirm the destructive warning')
+def step_confirm_destructive_command(context):
+ """Confirm destructive command."""
+ wrappers.expect_exact(
+ context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2)
+ context.cli.sendline('y')
+
+
+@when(u'we answer the destructive warning with "{confirmation}"')
+def step_confirm_destructive_command(context, confirmation):
+ """Confirm destructive command."""
+ wrappers.expect_exact(
+ context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2)
+ context.cli.sendline(confirmation)
+
+
+@then(u'we answer the destructive warning with invalid "{confirmation}" and see text "{text}"')
+def step_confirm_destructive_command(context, confirmation, text):
+ """Confirm destructive command."""
+ wrappers.expect_exact(
+ context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2)
+ context.cli.sendline(confirmation)
+ wrappers.expect_exact(context, text, timeout=2)
+ # we must exit the Click loop, or the feature will hang
+ context.cli.sendline('n')
diff --git a/test/features/steps/connection.py b/test/features/steps/connection.py
new file mode 100644
index 0000000..e16dd86
--- /dev/null
+++ b/test/features/steps/connection.py
@@ -0,0 +1,71 @@
+import io
+import os
+import shlex
+
+from behave import when, then
+import pexpect
+
+import wrappers
+from test.features.steps.utils import parse_cli_args_to_dict
+from test.features.environment import MY_CNF_PATH, MYLOGIN_CNF_PATH, get_db_name_from_context
+from test.utils import HOST, PORT, USER, PASSWORD
+from mycli.config import encrypt_mylogin_cnf
+
+
+TEST_LOGIN_PATH = 'test_login_path'
+
+
+@when('we run mycli with arguments "{exact_args}" without arguments "{excluded_args}"')
+@when('we run mycli without arguments "{excluded_args}"')
+def step_run_cli_without_args(context, excluded_args, exact_args=''):
+ wrappers.run_cli(
+ context,
+ run_args=parse_cli_args_to_dict(exact_args),
+ exclude_args=parse_cli_args_to_dict(excluded_args).keys()
+ )
+
+
+@then('status contains "{expression}"')
+def status_contains(context, expression):
+ wrappers.expect_exact(context, f'{expression}', timeout=5)
+
+ # Normally, the shutdown after scenario waits for the prompt.
+ # But we may have changed the prompt, depending on parameters,
+ # so let's wait for its last character
+ context.cli.expect_exact('>')
+ context.atprompt = True
+
+
+@when('we create my.cnf file')
+def step_create_my_cnf_file(context):
+ my_cnf = (
+ '[client]\n'
+ f'host = {HOST}\n'
+ f'port = {PORT}\n'
+ f'user = {USER}\n'
+ f'password = {PASSWORD}\n'
+ )
+ with open(MY_CNF_PATH, 'w') as f:
+ f.write(my_cnf)
+
+
+@when('we create mylogin.cnf file')
+def step_create_mylogin_cnf_file(context):
+ os.environ.pop('MYSQL_TEST_LOGIN_FILE', None)
+ mylogin_cnf = (
+ f'[{TEST_LOGIN_PATH}]\n'
+ f'host = {HOST}\n'
+ f'port = {PORT}\n'
+ f'user = {USER}\n'
+ f'password = {PASSWORD}\n'
+ )
+ with open(MYLOGIN_CNF_PATH, 'wb') as f:
+ input_file = io.StringIO(mylogin_cnf)
+ f.write(encrypt_mylogin_cnf(input_file).read())
+
+
+@then('we are logged in')
+def we_are_logged_in(context):
+ db_name = get_db_name_from_context(context)
+ context.cli.expect_exact(f'{db_name}>', timeout=5)
+ context.atprompt = True
diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py
new file mode 100644
index 0000000..841f37d
--- /dev/null
+++ b/test/features/steps/crud_database.py
@@ -0,0 +1,115 @@
+"""Steps for behavioral style tests are defined in this module.
+
+Each step is defined by the string decorating it. This string is used
+to call the step in "*.feature" file.
+
+"""
+
+import pexpect
+
+import wrappers
+from behave import when, then
+
+
+@when('we create database')
+def step_db_create(context):
+ """Send create database."""
+ context.cli.sendline('create database {0};'.format(
+ context.conf['dbname_tmp']))
+
+ context.response = {
+ 'database_name': context.conf['dbname_tmp']
+ }
+
+
+@when('we drop database')
+def step_db_drop(context):
+ """Send drop database."""
+ context.cli.sendline('drop database {0};'.format(
+ context.conf['dbname_tmp']))
+
+
+@when('we connect to test database')
+def step_db_connect_test(context):
+ """Send connect to database."""
+ db_name = context.conf['dbname']
+ context.currentdb = db_name
+ context.cli.sendline('use {0};'.format(db_name))
+
+
+@when('we connect to quoted test database')
+def step_db_connect_quoted_tmp(context):
+ """Send connect to database."""
+ db_name = context.conf['dbname']
+ context.currentdb = db_name
+ context.cli.sendline('use `{0}`;'.format(db_name))
+
+
+@when('we connect to tmp database')
+def step_db_connect_tmp(context):
+ """Send connect to database."""
+ db_name = context.conf['dbname_tmp']
+ context.currentdb = db_name
+ context.cli.sendline('use {0}'.format(db_name))
+
+
+@when('we connect to dbserver')
+def step_db_connect_dbserver(context):
+ """Send connect to database."""
+ context.currentdb = 'mysql'
+ context.cli.sendline('use mysql')
+
+
+@then('dbcli exits')
+def step_wait_exit(context):
+ """Make sure the cli exits."""
+ wrappers.expect_exact(context, pexpect.EOF, timeout=5)
+
+
+@then('we see dbcli prompt')
+def step_see_prompt(context):
+ """Wait to see the prompt."""
+ user = context.conf['user']
+ host = context.conf['host']
+ dbname = context.currentdb
+ wrappers.wait_prompt(context, '{0}@{1}:{2}> '.format(user, host, dbname))
+
+
+@then('we see help output')
+def step_see_help(context):
+ for expected_line in context.fixture_data['help_commands.txt']:
+ wrappers.expect_exact(context, expected_line, timeout=1)
+
+
+@then('we see database created')
+def step_see_db_created(context):
+ """Wait to see create database output."""
+ wrappers.expect_exact(context, 'Query OK, 1 row affected', timeout=2)
+
+
+@then('we see database dropped')
+def step_see_db_dropped(context):
+ """Wait to see drop database output."""
+ wrappers.expect_exact(context, 'Query OK, 0 rows affected', timeout=2)
+
+
+@then('we see database dropped and no default database')
+def step_see_db_dropped_no_default(context):
+ """Wait to see drop database output."""
+ user = context.conf['user']
+ host = context.conf['host']
+ database = '(none)'
+ context.currentdb = None
+
+ wrappers.expect_exact(context, 'Query OK, 0 rows affected', timeout=2)
+ wrappers.wait_prompt(context, '{0}@{1}:{2}>'.format(user, host, database))
+
+
+@then('we see database connected')
+def step_see_db_connected(context):
+ """Wait to see drop database output."""
+ wrappers.expect_exact(
+ context, 'You are now connected to database "', timeout=2)
+ wrappers.expect_exact(context, '"', timeout=2)
+ wrappers.expect_exact(context, ' as user "{0}"'.format(
+ context.conf['user']), timeout=2)
diff --git a/test/features/steps/crud_table.py b/test/features/steps/crud_table.py
new file mode 100644
index 0000000..f715f0c
--- /dev/null
+++ b/test/features/steps/crud_table.py
@@ -0,0 +1,112 @@
+"""Steps for behavioral style tests are defined in this module.
+
+Each step is defined by the string decorating it. This string is used
+to call the step in "*.feature" file.
+
+"""
+
+import wrappers
+from behave import when, then
+from textwrap import dedent
+
+
+@when('we create table')
+def step_create_table(context):
+ """Send create table."""
+ context.cli.sendline('create table a(x text);')
+
+
+@when('we insert into table')
+def step_insert_into_table(context):
+ """Send insert into table."""
+ context.cli.sendline('''insert into a(x) values('xxx');''')
+
+
+@when('we update table')
+def step_update_table(context):
+ """Send insert into table."""
+ context.cli.sendline('''update a set x = 'yyy' where x = 'xxx';''')
+
+
+@when('we select from table')
+def step_select_from_table(context):
+ """Send select from table."""
+ context.cli.sendline('select * from a;')
+
+
+@when('we delete from table')
+def step_delete_from_table(context):
+ """Send deete from table."""
+ context.cli.sendline('''delete from a where x = 'yyy';''')
+
+
+@when('we drop table')
+def step_drop_table(context):
+ """Send drop table."""
+ context.cli.sendline('drop table a;')
+
+
+@then('we see table created')
+def step_see_table_created(context):
+ """Wait to see create table output."""
+ wrappers.expect_exact(context, 'Query OK, 0 rows affected', timeout=2)
+
+
+@then('we see record inserted')
+def step_see_record_inserted(context):
+ """Wait to see insert output."""
+ wrappers.expect_exact(context, 'Query OK, 1 row affected', timeout=2)
+
+
+@then('we see record updated')
+def step_see_record_updated(context):
+ """Wait to see update output."""
+ wrappers.expect_exact(context, 'Query OK, 1 row affected', timeout=2)
+
+
+@then('we see data selected')
+def step_see_data_selected(context):
+ """Wait to see select output."""
+ wrappers.expect_pager(
+ context, dedent("""\
+ +-----+\r
+ | x |\r
+ +-----+\r
+ | yyy |\r
+ +-----+\r
+ \r
+ """), timeout=2)
+ wrappers.expect_exact(context, '1 row in set', timeout=2)
+
+
+@then('we see record deleted')
+def step_see_data_deleted(context):
+ """Wait to see delete output."""
+ wrappers.expect_exact(context, 'Query OK, 1 row affected', timeout=2)
+
+
+@then('we see table dropped')
+def step_see_table_dropped(context):
+ """Wait to see drop output."""
+ wrappers.expect_exact(context, 'Query OK, 0 rows affected', timeout=2)
+
+
+@when('we select null')
+def step_select_null(context):
+ """Send select null."""
+ context.cli.sendline('select null;')
+
+
+@then('we see null selected')
+def step_see_null_selected(context):
+ """Wait to see null output."""
+ wrappers.expect_pager(
+ context, dedent("""\
+ +--------+\r
+ | NULL |\r
+ +--------+\r
+ | <null> |\r
+ +--------+\r
+ \r
+ """), timeout=2)
+ wrappers.expect_exact(context, '1 row in set', timeout=2)
diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py
new file mode 100644
index 0000000..bbabf43
--- /dev/null
+++ b/test/features/steps/iocommands.py
@@ -0,0 +1,105 @@
+import os
+import wrappers
+
+from behave import when, then
+from textwrap import dedent
+
+
+@when('we start external editor providing a file name')
+def step_edit_file(context):
+ """Edit file with external editor."""
+ context.editor_file_name = os.path.join(
+ context.package_root, 'test_file_{0}.sql'.format(context.conf['vi']))
+ if os.path.exists(context.editor_file_name):
+ os.remove(context.editor_file_name)
+ context.cli.sendline('\e {0}'.format(
+ os.path.basename(context.editor_file_name)))
+ wrappers.expect_exact(
+ context, 'Entering Ex mode. Type "visual" to go to Normal mode.', timeout=2)
+ wrappers.expect_exact(context, '\r\n:', timeout=2)
+
+
+@when('we type "{query}" in the editor')
+def step_edit_type_sql(context, query):
+ context.cli.sendline('i')
+ context.cli.sendline(query)
+ context.cli.sendline('.')
+ wrappers.expect_exact(context, '\r\n:', timeout=2)
+
+
+@when('we exit the editor')
+def step_edit_quit(context):
+ context.cli.sendline('x')
+ wrappers.expect_exact(context, "written", timeout=2)
+
+
+@then('we see "{query}" in prompt')
+def step_edit_done_sql(context, query):
+ for match in query.split(' '):
+ wrappers.expect_exact(context, match, timeout=5)
+ # Cleanup the command line.
+ context.cli.sendcontrol('c')
+ # Cleanup the edited file.
+ if context.editor_file_name and os.path.exists(context.editor_file_name):
+ os.remove(context.editor_file_name)
+
+
+@when(u'we tee output')
+def step_tee_ouptut(context):
+ context.tee_file_name = os.path.join(
+ context.package_root, 'tee_file_{0}.sql'.format(context.conf['vi']))
+ if os.path.exists(context.tee_file_name):
+ os.remove(context.tee_file_name)
+ context.cli.sendline('tee {0}'.format(
+ os.path.basename(context.tee_file_name)))
+
+
+@when(u'we select "select {param}"')
+def step_query_select_number(context, param):
+ context.cli.sendline(u'select {}'.format(param))
+ wrappers.expect_pager(context, dedent(u"""\
+ +{dashes}+\r
+ | {param} |\r
+ +{dashes}+\r
+ | {param} |\r
+ +{dashes}+\r
+ \r
+ """.format(param=param, dashes='-' * (len(param) + 2))
+ ), timeout=5)
+ wrappers.expect_exact(context, '1 row in set', timeout=2)
+
+
+@then(u'we see result "{result}"')
+def step_see_result(context, result):
+ wrappers.expect_exact(
+ context,
+ u"| {} |".format(result),
+ timeout=2
+ )
+
+
+@when(u'we query "{query}"')
+def step_query(context, query):
+ context.cli.sendline(query)
+
+
+@when(u'we notee output')
+def step_notee_output(context):
+ context.cli.sendline('notee')
+
+
+@then(u'we see 123456 in tee output')
+def step_see_123456_in_ouput(context):
+ with open(context.tee_file_name) as f:
+ assert '123456' in f.read()
+ if os.path.exists(context.tee_file_name):
+ os.remove(context.tee_file_name)
+
+
+@then(u'delimiter is set to "{delimiter}"')
+def delimiter_is_set(context, delimiter):
+ wrappers.expect_exact(
+ context,
+ u'Changed delimiter to {}'.format(delimiter),
+ timeout=2
+ )
diff --git a/test/features/steps/named_queries.py b/test/features/steps/named_queries.py
new file mode 100644
index 0000000..bc1f866
--- /dev/null
+++ b/test/features/steps/named_queries.py
@@ -0,0 +1,90 @@
+"""Steps for behavioral style tests are defined in this module.
+
+Each step is defined by the string decorating it. This string is used
+to call the step in "*.feature" file.
+
+"""
+
+import wrappers
+from behave import when, then
+
+
+@when('we save a named query')
+def step_save_named_query(context):
+ """Send \fs command."""
+ context.cli.sendline('\\fs foo SELECT 12345')
+
+
+@when('we use a named query')
+def step_use_named_query(context):
+ """Send \f command."""
+ context.cli.sendline('\\f foo')
+
+
+@when('we delete a named query')
+def step_delete_named_query(context):
+ """Send \fd command."""
+ context.cli.sendline('\\fd foo')
+
+
+@then('we see the named query saved')
+def step_see_named_query_saved(context):
+ """Wait to see query saved."""
+ wrappers.expect_exact(context, 'Saved.', timeout=2)
+
+
+@then('we see the named query executed')
+def step_see_named_query_executed(context):
+ """Wait to see select output."""
+ wrappers.expect_exact(context, 'SELECT 12345', timeout=2)
+
+
+@then('we see the named query deleted')
+def step_see_named_query_deleted(context):
+ """Wait to see query deleted."""
+ wrappers.expect_exact(context, 'foo: Deleted', timeout=2)
+
+
+@when('we save a named query with parameters')
+def step_save_named_query_with_parameters(context):
+ """Send \fs command for query with parameters."""
+ context.cli.sendline('\\fs foo_args SELECT $1, "$2", "$3"')
+
+
+@when('we use named query with parameters')
+def step_use_named_query_with_parameters(context):
+ """Send \f command with parameters."""
+ context.cli.sendline('\\f foo_args 101 second "third value"')
+
+
+@then('we see the named query with parameters executed')
+def step_see_named_query_with_parameters_executed(context):
+ """Wait to see select output."""
+ wrappers.expect_exact(
+ context, 'SELECT 101, "second", "third value"', timeout=2)
+
+
+@when('we use named query with too few parameters')
+def step_use_named_query_with_too_few_parameters(context):
+ """Send \f command with missing parameters."""
+ context.cli.sendline('\\f foo_args 101')
+
+
+@then('we see the named query with parameters fail with missing parameters')
+def step_see_named_query_with_parameters_fail_with_missing_parameters(context):
+ """Wait to see select output."""
+ wrappers.expect_exact(
+ context, 'missing substitution for $2 in query:', timeout=2)
+
+
+@when('we use named query with too many parameters')
+def step_use_named_query_with_too_many_parameters(context):
+ """Send \f command with extra parameters."""
+ context.cli.sendline('\\f foo_args 101 102 103 104')
+
+
+@then('we see the named query with parameters fail with extra parameters')
+def step_see_named_query_with_parameters_fail_with_extra_parameters(context):
+ """Wait to see select output."""
+ wrappers.expect_exact(
+ context, 'query does not have substitution parameter $4:', timeout=2)
diff --git a/test/features/steps/specials.py b/test/features/steps/specials.py
new file mode 100644
index 0000000..e8b99e3
--- /dev/null
+++ b/test/features/steps/specials.py
@@ -0,0 +1,27 @@
+"""Steps for behavioral style tests are defined in this module.
+
+Each step is defined by the string decorating it. This string is used
+to call the step in "*.feature" file.
+
+"""
+
+import wrappers
+from behave import when, then
+
+
+@when('we refresh completions')
+def step_refresh_completions(context):
+ """Send refresh command."""
+ context.cli.sendline('rehash')
+
+
+@then('we see text "{text}"')
+def step_see_text(context, text):
+ """Wait to see given text message."""
+ wrappers.expect_exact(context, text, timeout=2)
+
+@then('we see completions refresh started')
+def step_see_refresh_started(context):
+ """Wait to see refresh output."""
+ wrappers.expect_exact(
+ context, 'Auto-completion refresh started in the background.', timeout=2)
diff --git a/test/features/steps/utils.py b/test/features/steps/utils.py
new file mode 100644
index 0000000..1ae63d2
--- /dev/null
+++ b/test/features/steps/utils.py
@@ -0,0 +1,12 @@
+import shlex
+
+
+def parse_cli_args_to_dict(cli_args: str):
+ args_dict = {}
+ for arg in shlex.split(cli_args):
+ if '=' in arg:
+ key, value = arg.split('=')
+ args_dict[key] = value
+ else:
+ args_dict[arg] = None
+ return args_dict
diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py
new file mode 100644
index 0000000..6408f23
--- /dev/null
+++ b/test/features/steps/wrappers.py
@@ -0,0 +1,117 @@
+import re
+import pexpect
+import sys
+import textwrap
+
+
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+
+def expect_exact(context, expected, timeout):
+ timedout = False
+ try:
+ context.cli.expect_exact(expected, timeout=timeout)
+ except pexpect.TIMEOUT:
+ timedout = True
+ if timedout:
+ # Strip color codes out of the output.
+ actual = re.sub(r'\x1b\[([0-9A-Za-z;?])+[m|K]?',
+ '', context.cli.before)
+ raise Exception(
+ textwrap.dedent('''\
+ Expected:
+ ---
+ {0!r}
+ ---
+ Actual:
+ ---
+ {1!r}
+ ---
+ Full log:
+ ---
+ {2!r}
+ ---
+ ''').format(
+ expected,
+ actual,
+ context.logfile.getvalue()
+ )
+ )
+
+
+def expect_pager(context, expected, timeout):
+ expect_exact(context, "{0}\r\n{1}{0}\r\n".format(
+ context.conf['pager_boundary'], expected), timeout=timeout)
+
+
+def run_cli(context, run_args=None, exclude_args=None):
+ """Run the process using pexpect."""
+ run_args = run_args or {}
+ rendered_args = []
+ exclude_args = set(exclude_args) if exclude_args else set()
+
+ conf = dict(**context.conf)
+ conf.update(run_args)
+
+ def add_arg(name, key, value):
+ if name not in exclude_args:
+ if value is not None:
+ rendered_args.extend((key, value))
+ else:
+ rendered_args.append(key)
+
+ if conf.get('host', None):
+ add_arg('host', '-h', conf['host'])
+ if conf.get('user', None):
+ add_arg('user', '-u', conf['user'])
+ if conf.get('pass', None):
+ add_arg('pass', '-p', conf['pass'])
+ if conf.get('port', None):
+ add_arg('port', '-P', str(conf['port']))
+ if conf.get('dbname', None):
+ add_arg('dbname', '-D', conf['dbname'])
+ if conf.get('defaults-file', None):
+ add_arg('defaults_file', '--defaults-file', conf['defaults-file'])
+ if conf.get('myclirc', None):
+ add_arg('myclirc', '--myclirc', conf['myclirc'])
+ if conf.get('login_path'):
+ add_arg('login_path', '--login-path', conf['login_path'])
+
+ for arg_name, arg_value in conf.items():
+ if arg_name.startswith('-'):
+ add_arg(arg_name, arg_name, arg_value)
+
+ try:
+ cli_cmd = context.conf['cli_command']
+ except KeyError:
+ cli_cmd = (
+ '{0!s} -c "'
+ 'import coverage ; '
+ 'coverage.process_startup(); '
+ 'import mycli.main; '
+ 'mycli.main.cli()'
+ '"'
+ ).format(sys.executable)
+
+ cmd_parts = [cli_cmd] + rendered_args
+ cmd = ' '.join(cmd_parts)
+ context.cli = pexpect.spawnu(cmd, cwd=context.package_root)
+ context.logfile = StringIO()
+ context.cli.logfile = context.logfile
+ context.exit_sent = False
+ context.currentdb = context.conf['dbname']
+
+
+def wait_prompt(context, prompt=None):
+ """Make sure prompt is displayed."""
+ if prompt is None:
+ user = context.conf['user']
+ host = context.conf['host']
+ dbname = context.currentdb
+ prompt = '{0}@{1}:{2}>'.format(
+ user, host, dbname),
+ expect_exact(context, prompt, timeout=5)
+ context.atprompt = True
diff --git a/test/features/wrappager.py b/test/features/wrappager.py
new file mode 100755
index 0000000..51d4909
--- /dev/null
+++ b/test/features/wrappager.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+import sys
+
+
+def wrappager(boundary):
+ print(boundary)
+ while 1:
+ buf = sys.stdin.read(2048)
+ if not buf:
+ break
+ sys.stdout.write(buf)
+ print(boundary)
+
+
+if __name__ == "__main__":
+ wrappager(sys.argv[1])