From 917739023a7acaae3645bbfd27ed454df3c5be33 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 20 Sep 2022 17:46:57 +0200 Subject: Adding upstream version 3.5.0. Signed-off-by: Daniel Baumann --- tests/features/basic_commands.feature | 1 - tests/features/db_utils.py | 37 ++++++----- tests/features/environment.py | 36 ++++++++--- tests/features/pgbouncer.feature | 12 ++++ tests/features/steps/basic_commands.py | 2 +- tests/features/steps/crud_database.py | 2 +- tests/features/steps/pgbouncer.py | 22 +++++++ tests/features/steps/wrappers.py | 3 +- tests/formatter/__init__.py | 1 + tests/formatter/test_sqlformatter.py | 111 +++++++++++++++++++++++++++++++++ tests/test_auth.py | 40 ++++++++++++ tests/test_exceptionals.py | 0 tests/test_pgexecute.py | 88 ++++++++++++++++++++++---- tests/test_plan.wiki | 38 ----------- tests/test_rowlimit.py | 2 +- tests/test_sqlcompletion.py | 2 +- tests/utils.py | 17 +++-- 17 files changed, 327 insertions(+), 87 deletions(-) create mode 100644 tests/features/pgbouncer.feature create mode 100644 tests/features/steps/pgbouncer.py create mode 100644 tests/formatter/__init__.py create mode 100644 tests/formatter/test_sqlformatter.py create mode 100644 tests/test_auth.py delete mode 100644 tests/test_exceptionals.py delete mode 100644 tests/test_plan.wiki (limited to 'tests') diff --git a/tests/features/basic_commands.feature b/tests/features/basic_commands.feature index 99f893e..cd15306 100644 --- a/tests/features/basic_commands.feature +++ b/tests/features/basic_commands.feature @@ -49,7 +49,6 @@ Feature: run the cli, when we send "\?" command then we see help output - @wip Scenario: run the cli with dsn and password When we launch dbcli using dsn_password then we send password diff --git a/tests/features/db_utils.py b/tests/features/db_utils.py index 6898394..595c6c2 100644 --- a/tests/features/db_utils.py +++ b/tests/features/db_utils.py @@ -1,5 +1,4 @@ -from psycopg2 import connect -from psycopg2.extensions import AsIs +from psycopg import connect def create_db( @@ -17,13 +16,10 @@ def create_db( """ cn = create_cn(hostname, password, username, "postgres", port) - # ISOLATION_LEVEL_AUTOCOMMIT = 0 - # Needed for DB creation. - cn.set_isolation_level(0) - + cn.autocommit = True with cn.cursor() as cr: - cr.execute("drop database if exists %s", (AsIs(dbname),)) - cr.execute("create database %s", (AsIs(dbname),)) + cr.execute(f"drop database if exists {dbname}") + cr.execute(f"create database {dbname}") cn.close() @@ -41,13 +37,26 @@ def create_cn(hostname, password, username, dbname, port): :return: psycopg2.connection """ cn = connect( - host=hostname, user=username, database=dbname, password=password, port=port + host=hostname, user=username, dbname=dbname, password=password, port=port ) - print(f"Created connection: {cn.dsn}.") + print(f"Created connection: {cn.info.get_parameters()}.") return cn +def pgbouncer_available(hostname="localhost", password=None, username="postgres"): + cn = None + try: + cn = create_cn(hostname, password, username, "pgbouncer", 6432) + return True + except: + print("Pgbouncer is not available.") + finally: + if cn: + cn.close() + return False + + def drop_db(hostname="localhost", username=None, password=None, dbname=None, port=None): """ Drop database. @@ -58,12 +67,11 @@ def drop_db(hostname="localhost", username=None, password=None, dbname=None, por """ cn = create_cn(hostname, password, username, "postgres", port) - # ISOLATION_LEVEL_AUTOCOMMIT = 0 # Needed for DB drop. - cn.set_isolation_level(0) + cn.autocommit = True with cn.cursor() as cr: - cr.execute("drop database if exists %s", (AsIs(dbname),)) + cr.execute(f"drop database if exists {dbname}") close_cn(cn) @@ -74,5 +82,6 @@ def close_cn(cn=None): :param connection: psycopg2.connection """ if cn: + cn_params = cn.info.get_parameters() cn.close() - print(f"Closed connection: {cn.dsn}.") + print(f"Closed connection: {cn_params}.") diff --git a/tests/features/environment.py b/tests/features/environment.py index 215c85c..6cc8e14 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -111,7 +111,11 @@ def before_all(context): context.conf["dbname"], context.conf["port"], ) - + context.pgbouncer_available = dbutils.pgbouncer_available( + hostname=context.conf["host"], + password=context.conf["pass"], + username=context.conf["user"], + ) context.fixture_data = fixutils.read_fixture_files() # use temporary directory as config home @@ -145,7 +149,7 @@ def after_all(context): context.conf["port"], ) - # Remove temp config direcotry + # Remove temp config directory shutil.rmtree(context.env_config_home) # Restore env vars. @@ -164,7 +168,19 @@ def before_scenario(context, scenario): if scenario.name == "list databases": # not using the cli for that return - wrappers.run_cli(context) + currentdb = None + if "pgbouncer" in scenario.feature.tags: + if context.pgbouncer_available: + os.environ["PGDATABASE"] = "pgbouncer" + os.environ["PGPORT"] = "6432" + currentdb = "pgbouncer" + else: + scenario.skip() + else: + # set env vars back to normal test database + os.environ["PGDATABASE"] = context.conf["dbname"] + os.environ["PGPORT"] = context.conf["port"] + wrappers.run_cli(context, currentdb=currentdb) wrappers.wait_prompt(context) @@ -172,13 +188,17 @@ def after_scenario(context, scenario): """Cleans up after each scenario completes.""" if hasattr(context, "cli") and context.cli and not context.exit_sent: # Quit nicely. - if not context.atprompt: + if not getattr(context, "atprompt", False): dbname = context.currentdb - context.cli.expect_exact(f"{dbname}> ", timeout=15) - context.cli.sendcontrol("c") - context.cli.sendcontrol("d") + context.cli.expect_exact(f"{dbname}>", timeout=5) + try: + context.cli.sendcontrol("c") + context.cli.sendcontrol("d") + except Exception as x: + print("Failed cleanup after scenario:") + print(x) try: - context.cli.expect_exact(pexpect.EOF, timeout=15) + context.cli.expect_exact(pexpect.EOF, timeout=5) except pexpect.TIMEOUT: print(f"--- after_scenario {scenario.name}: kill cli") context.cli.kill(signal.SIGKILL) diff --git a/tests/features/pgbouncer.feature b/tests/features/pgbouncer.feature new file mode 100644 index 0000000..14cc5ad --- /dev/null +++ b/tests/features/pgbouncer.feature @@ -0,0 +1,12 @@ +@pgbouncer +Feature: run pgbouncer, + call the help command, + exit the cli + + Scenario: run "show help" command + When we send "show help" command + then we see the pgbouncer help output + + Scenario: run the cli and exit + When we send "ctrl + d" + then dbcli exits diff --git a/tests/features/steps/basic_commands.py b/tests/features/steps/basic_commands.py index a7c99ee..7c87814 100644 --- a/tests/features/steps/basic_commands.py +++ b/tests/features/steps/basic_commands.py @@ -69,7 +69,7 @@ def step_ctrl_d(context): context.cli.sendline(r"\pset pager off") wrappers.wait_prompt(context) context.cli.sendcontrol("d") - context.cli.expect(pexpect.EOF, timeout=15) + context.cli.expect(pexpect.EOF, timeout=5) context.exit_sent = True diff --git a/tests/features/steps/crud_database.py b/tests/features/steps/crud_database.py index 3f5d0e7..87cdc85 100644 --- a/tests/features/steps/crud_database.py +++ b/tests/features/steps/crud_database.py @@ -59,7 +59,7 @@ def step_see_prompt(context): Wait to see the prompt. """ db_name = getattr(context, "currentdb", context.conf["dbname"]) - wrappers.expect_exact(context, f"{db_name}> ", timeout=5) + wrappers.expect_exact(context, f"{db_name}>", timeout=5) context.atprompt = True diff --git a/tests/features/steps/pgbouncer.py b/tests/features/steps/pgbouncer.py new file mode 100644 index 0000000..f156982 --- /dev/null +++ b/tests/features/steps/pgbouncer.py @@ -0,0 +1,22 @@ +""" +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, then +import wrappers + + +@when('we send "show help" command') +def step_send_help_command(context): + context.cli.sendline("show help") + + +@then("we see the pgbouncer help output") +def see_pgbouncer_help(context): + wrappers.expect_exact( + context, + "SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION", + timeout=3, + ) diff --git a/tests/features/steps/wrappers.py b/tests/features/steps/wrappers.py index 0ca8366..6180517 100644 --- a/tests/features/steps/wrappers.py +++ b/tests/features/steps/wrappers.py @@ -70,4 +70,5 @@ def run_cli(context, run_args=None, prompt_check=True, currentdb=None): def wait_prompt(context): """Make sure prompt is displayed.""" - expect_exact(context, "{0}> ".format(context.conf["dbname"]), timeout=5) + prompt_str = "{0}>".format(context.currentdb) + expect_exact(context, [prompt_str + " ", prompt_str, pexpect.EOF], timeout=3) diff --git a/tests/formatter/__init__.py b/tests/formatter/__init__.py new file mode 100644 index 0000000..9bad579 --- /dev/null +++ b/tests/formatter/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/tests/formatter/test_sqlformatter.py b/tests/formatter/test_sqlformatter.py new file mode 100644 index 0000000..b8cd9c2 --- /dev/null +++ b/tests/formatter/test_sqlformatter.py @@ -0,0 +1,111 @@ +# coding=utf-8 + +from pgcli.packages.formatter.sqlformatter import escape_for_sql_statement + +from cli_helpers.tabular_output import TabularOutputFormatter +from pgcli.packages.formatter.sqlformatter import adapter, register_new_formatter + + +def test_escape_for_sql_statement_bytes(): + bts = b"837124ab3e8dc0f" + escaped_bytes = escape_for_sql_statement(bts) + assert escaped_bytes == "X'383337313234616233653864633066'" + + +def test_escape_for_sql_statement_number(): + num = 2981 + escaped_bytes = escape_for_sql_statement(num) + assert escaped_bytes == "'2981'" + + +def test_escape_for_sql_statement_str(): + example_str = "example str" + escaped_bytes = escape_for_sql_statement(example_str) + assert escaped_bytes == "'example str'" + + +def test_output_sql_insert(): + global formatter + formatter = TabularOutputFormatter + register_new_formatter(formatter) + data = [ + [ + 1, + "Jackson", + "jackson_test@gmail.com", + "132454789", + "", + "2022-09-09 19:44:32.712343+08", + "2022-09-09 19:44:32.712343+08", + ] + ] + header = ["id", "name", "email", "phone", "description", "created_at", "updated_at"] + table_format = "sql-insert" + kwargs = { + "column_types": [int, str, str, str, str, str, str], + "sep_title": "RECORD {n}", + "sep_character": "-", + "sep_length": (1, 25), + "missing_value": "", + "integer_format": "", + "float_format": "", + "disable_numparse": True, + "preserve_whitespace": True, + "max_field_width": 500, + } + formatter.query = 'SELECT * FROM "user";' + output = adapter(data, header, table_format=table_format, **kwargs) + output_list = [l for l in output] + expected = [ + 'INSERT INTO "user" ("id", "name", "email", "phone", "description", "created_at", "updated_at") VALUES', + " ('1', 'Jackson', 'jackson_test@gmail.com', '132454789', '', " + + "'2022-09-09 19:44:32.712343+08', '2022-09-09 19:44:32.712343+08')", + ";", + ] + assert expected == output_list + + +def test_output_sql_update(): + global formatter + formatter = TabularOutputFormatter + register_new_formatter(formatter) + data = [ + [ + 1, + "Jackson", + "jackson_test@gmail.com", + "132454789", + "", + "2022-09-09 19:44:32.712343+08", + "2022-09-09 19:44:32.712343+08", + ] + ] + header = ["id", "name", "email", "phone", "description", "created_at", "updated_at"] + table_format = "sql-update" + kwargs = { + "column_types": [int, str, str, str, str, str, str], + "sep_title": "RECORD {n}", + "sep_character": "-", + "sep_length": (1, 25), + "missing_value": "", + "integer_format": "", + "float_format": "", + "disable_numparse": True, + "preserve_whitespace": True, + "max_field_width": 500, + } + formatter.query = 'SELECT * FROM "user";' + output = adapter(data, header, table_format=table_format, **kwargs) + output_list = [l for l in output] + print(output_list) + expected = [ + 'UPDATE "user" SET', + " \"name\" = 'Jackson'", + ", \"email\" = 'jackson_test@gmail.com'", + ", \"phone\" = '132454789'", + ", \"description\" = ''", + ", \"created_at\" = '2022-09-09 19:44:32.712343+08'", + ", \"updated_at\" = '2022-09-09 19:44:32.712343+08'", + "WHERE \"id\" = '1';", + ] + assert expected == output_list diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..a517a89 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,40 @@ +import pytest +from unittest import mock +from pgcli import auth + + +@pytest.mark.parametrize("enabled,call_count", [(True, 1), (False, 0)]) +def test_keyring_initialize(enabled, call_count): + logger = mock.MagicMock() + + with mock.patch("importlib.import_module", return_value=True) as import_method: + auth.keyring_initialize(enabled, logger=logger) + assert import_method.call_count == call_count + + +def test_keyring_get_password_ok(): + with mock.patch("pgcli.auth.keyring", return_value=mock.MagicMock()): + with mock.patch("pgcli.auth.keyring.get_password", return_value="abc123"): + assert auth.keyring_get_password("test") == "abc123" + + +def test_keyring_get_password_exception(): + with mock.patch("pgcli.auth.keyring", return_value=mock.MagicMock()): + with mock.patch( + "pgcli.auth.keyring.get_password", side_effect=Exception("Boom!") + ): + assert auth.keyring_get_password("test") == "" + + +def test_keyring_set_password_ok(): + with mock.patch("pgcli.auth.keyring", return_value=mock.MagicMock()): + with mock.patch("pgcli.auth.keyring.set_password"): + auth.keyring_set_password("test", "abc123") + + +def test_keyring_set_password_exception(): + with mock.patch("pgcli.auth.keyring", return_value=mock.MagicMock()): + with mock.patch( + "pgcli.auth.keyring.set_password", side_effect=Exception("Boom!") + ): + auth.keyring_set_password("test", "abc123") diff --git a/tests/test_exceptionals.py b/tests/test_exceptionals.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_pgexecute.py b/tests/test_pgexecute.py index 109674c..d6d2f93 100644 --- a/tests/test_pgexecute.py +++ b/tests/test_pgexecute.py @@ -1,6 +1,6 @@ from textwrap import dedent -import psycopg2 +import psycopg import pytest from unittest.mock import patch, MagicMock from pgspecial.main import PGSpecial, NO_QUERY @@ -282,6 +282,77 @@ def test_execute_from_file_io_error(os, executor, pgspecial): assert is_special == True +@dbtest +def test_execute_from_commented_file_that_executes_another_file( + executor, pgspecial, tmpdir +): + # https://github.com/dbcli/pgcli/issues/1336 + sqlfile1 = tmpdir.join("test01.sql") + sqlfile1.write("-- asdf \n\\h") + sqlfile2 = tmpdir.join("test00.sql") + sqlfile2.write("--An useless comment;\nselect now();\n-- another useless comment") + + rcfile = str(tmpdir.join("rcfile")) + print(rcfile) + cli = PGCli(pgexecute=executor, pgclirc_file=rcfile) + assert cli != None + statement = "--comment\n\\h" + result = run(executor, statement, pgspecial=cli.pgspecial) + assert result != None + assert result[0].find("ALTER TABLE") + + +@dbtest +def test_execute_commented_first_line_and_special(executor, pgspecial, tmpdir): + # https://github.com/dbcli/pgcli/issues/1362 + + # just some base caes that should work also + statement = "--comment\nselect now();" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("now") >= 0 + + statement = "/*comment*/\nselect now();" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("now") >= 0 + + statement = "/*comment\ncomment line2*/\nselect now();" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("now") >= 0 + + statement = "--comment\n\\h" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 + + statement = "/*comment*/\n\h;" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 + + statement = " /*comment*/\n\h;" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 + + statement = "/*comment\ncomment line2*/\n\h;" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 + + statement = " /*comment\ncomment line2*/\n\h;" + result = run(executor, statement, pgspecial=pgspecial) + assert result != None + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 + + @dbtest def test_multiple_queries_same_line(executor): result = run(executor, "select 'foo'; select 'bar'") @@ -428,7 +499,7 @@ def test_describe_special(executor, command, verbose, pattern, pgspecial): @dbtest @pytest.mark.parametrize("sql", ["invalid sql", "SELECT 1; select error;"]) def test_raises_with_no_formatter(executor, sql): - with pytest.raises(psycopg2.ProgrammingError): + with pytest.raises(psycopg.ProgrammingError): list(executor.run(sql)) @@ -513,13 +584,6 @@ def test_short_host(executor): assert executor.short_host == "localhost1" -class BrokenConnection: - """Mock a connection that failed.""" - - def cursor(self): - raise psycopg2.InterfaceError("I'm broken!") - - class VirtualCursor: """Mock a cursor to virtual database like pgbouncer.""" @@ -549,13 +613,15 @@ def test_exit_without_active_connection(executor): aliases=(":q",), ) - with patch.object(executor, "conn", BrokenConnection()): + with patch.object( + executor.conn, "cursor", side_effect=psycopg.InterfaceError("I'm broken!") + ): # we should be able to quit the app, even without active connection run(executor, "\\q", pgspecial=pgspecial) quit_handler.assert_called_once() # an exception should be raised when running a query without active connection - with pytest.raises(psycopg2.InterfaceError): + with pytest.raises(psycopg.InterfaceError): run(executor, "select 1", pgspecial=pgspecial) diff --git a/tests/test_plan.wiki b/tests/test_plan.wiki deleted file mode 100644 index 6812f18..0000000 --- a/tests/test_plan.wiki +++ /dev/null @@ -1,38 +0,0 @@ -= Gross Checks = - * [ ] Check connecting to a local database. - * [ ] Check connecting to a remote database. - * [ ] Check connecting to a database with a user/password. - * [ ] Check connecting to a non-existent database. - * [ ] Test changing the database. - - == PGExecute == - * [ ] Test successful execution given a cursor. - * [ ] Test unsuccessful execution with a syntax error. - * [ ] Test a series of executions with the same cursor without failure. - * [ ] Test a series of executions with the same cursor with failure. - * [ ] Test passing in a special command. - - == Naive Autocompletion == - * [ ] Input empty string, ask for completions - Everything. - * [ ] Input partial prefix, ask for completions - Stars with prefix. - * [ ] Input fully autocompleted string, ask for completions - Only full match - * [ ] Input non-existent prefix, ask for completions - nothing - * [ ] Input lowercase prefix - case insensitive completions - - == Smart Autocompletion == - * [ ] Input empty string and check if only keywords are returned. - * [ ] Input SELECT prefix and check if only columns are returned. - * [ ] Input SELECT blah - only keywords are returned. - * [ ] Input SELECT * FROM - Table names only - - == PGSpecial == - * [ ] Test \d - * [ ] Test \d tablename - * [ ] Test \d tablena* - * [ ] Test \d non-existent-tablename - * [ ] Test \d index - * [ ] Test \d sequence - * [ ] Test \d view - - == Exceptionals == - * [ ] Test the 'use' command to change db. diff --git a/tests/test_rowlimit.py b/tests/test_rowlimit.py index 947fc80..da916b4 100644 --- a/tests/test_rowlimit.py +++ b/tests/test_rowlimit.py @@ -4,7 +4,7 @@ from unittest.mock import Mock from pgcli.main import PGCli -# We need this fixtures beacause we need PGCli object to be created +# We need this fixtures because we need PGCli object to be created # after test collection so it has config loaded from temp directory diff --git a/tests/test_sqlcompletion.py b/tests/test_sqlcompletion.py index 744fadb..1034bbe 100644 --- a/tests/test_sqlcompletion.py +++ b/tests/test_sqlcompletion.py @@ -844,7 +844,7 @@ def test_alter_column_type_suggests_types(): "CREATE FUNCTION foo (bar INT, baz ", "SELECT * FROM foo() AS bar (baz ", "SELECT * FROM foo() AS bar (baz INT, qux ", - # make sure this doesnt trigger special completion + # make sure this doesn't trigger special completion "CREATE TABLE foo (dt d", ], ) diff --git a/tests/utils.py b/tests/utils.py index 460ea46..67d769f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,6 @@ import pytest -import psycopg2 -import psycopg2.extras +import psycopg from pgcli.main import format_output, OutputSettings -from pgcli.pgexecute import register_json_typecasters from os import getenv POSTGRES_USER = getenv("PGUSER", "postgres") @@ -12,12 +10,12 @@ POSTGRES_PASSWORD = getenv("PGPASSWORD", "postgres") def db_connection(dbname=None): - conn = psycopg2.connect( + conn = psycopg.connect( user=POSTGRES_USER, host=POSTGRES_HOST, password=POSTGRES_PASSWORD, port=POSTGRES_PORT, - database=dbname, + dbname=dbname, ) conn.autocommit = True return conn @@ -26,11 +24,10 @@ def db_connection(dbname=None): try: conn = db_connection() CAN_CONNECT_TO_DB = True - SERVER_VERSION = conn.server_version - json_types = register_json_typecasters(conn, lambda x: x) - JSON_AVAILABLE = "json" in json_types - JSONB_AVAILABLE = "jsonb" in json_types -except: + SERVER_VERSION = conn.info.parameter_status("server_version") + JSON_AVAILABLE = True + JSONB_AVAILABLE = True +except Exception as x: CAN_CONNECT_TO_DB = JSON_AVAILABLE = JSONB_AVAILABLE = False SERVER_VERSION = 0 -- cgit v1.2.3