1
0
Fork 0

Merging upstream version 3.5.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 20:01:39 +01:00
parent 7a56138e00
commit 6bbbbdf0c7
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
43 changed files with 1272 additions and 430 deletions

View file

@ -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

View file

@ -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}.")

View file

@ -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.expect_exact(pexpect.EOF, timeout=15)
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=5)
except pexpect.TIMEOUT:
print(f"--- after_scenario {scenario.name}: kill cli")
context.cli.kill(signal.SIGKILL)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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)

View file

@ -0,0 +1 @@
# coding=utf-8

View file

@ -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": "<null>",
"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": "<null>",
"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

40
tests/test_auth.py Normal file
View file

@ -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")

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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",
],
)

View file

@ -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