From b87ee950e3a0c43769bf29f3a98eba9887fb5564 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 18 May 2024 15:21:44 +0200 Subject: Merging upstream version 4.1.0. Signed-off-by: Daniel Baumann --- tests/conftest.py | 2 + tests/features/steps/crud_database.py | 1 + tests/test_application_name.py | 17 ++++++ tests/test_main.py | 99 ++++++++++++++++++++++++++++++++++- tests/test_pgexecute.py | 66 ++++++++++++++++++++++- tests/test_ssh_tunnel.py | 4 +- 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 tests/test_application_name.py (limited to 'tests') diff --git a/tests/conftest.py b/tests/conftest.py index 33cddf2..e50f1fe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from utils import ( db_connection, drop_tables, ) +import pgcli.main import pgcli.pgexecute @@ -37,6 +38,7 @@ def executor(connection): password=POSTGRES_PASSWORD, port=POSTGRES_PORT, dsn=None, + notify_callback=pgcli.main.notify_callback, ) diff --git a/tests/features/steps/crud_database.py b/tests/features/steps/crud_database.py index 87cdc85..9507d46 100644 --- a/tests/features/steps/crud_database.py +++ b/tests/features/steps/crud_database.py @@ -3,6 +3,7 @@ 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 from behave import when, then diff --git a/tests/test_application_name.py b/tests/test_application_name.py new file mode 100644 index 0000000..5fac5b2 --- /dev/null +++ b/tests/test_application_name.py @@ -0,0 +1,17 @@ +from unittest.mock import patch + +from click.testing import CliRunner + +from pgcli.main import cli +from pgcli.pgexecute import PGExecute + + +def test_application_name_in_env(): + runner = CliRunner() + app_name = "wonderful_app" + with patch.object(PGExecute, "__init__") as mock_pgxecute: + runner.invoke( + cli, ["127.0.0.1:5432/hello", "user"], env={"PGAPPNAME": app_name} + ) + kwargs = mock_pgxecute.call_args.kwargs + assert kwargs.get("application_name") == app_name diff --git a/tests/test_main.py b/tests/test_main.py index cbf20a6..3683d49 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,8 @@ import os import platform +import re +import tempfile +import datetime from unittest import mock import pytest @@ -11,7 +14,9 @@ except ImportError: from pgcli.main import ( obfuscate_process_password, + duration_in_words, format_output, + notify_callback, PGCli, OutputSettings, COLOR_CODE_REGEX, @@ -296,6 +301,24 @@ def test_i_works(tmpdir, executor): run(executor, statement, pgspecial=cli.pgspecial) +@dbtest +def test_toggle_verbose_errors(executor): + cli = PGCli(pgexecute=executor) + + cli._evaluate_command("\\v on") + assert cli.verbose_errors + output, _ = cli._evaluate_command("SELECT 1/0") + assert "SQLSTATE" in output[0] + + cli._evaluate_command("\\v off") + assert not cli.verbose_errors + output, _ = cli._evaluate_command("SELECT 1/0") + assert "SQLSTATE" not in output[0] + + cli._evaluate_command("\\v") + assert cli.verbose_errors + + @dbtest def test_echo_works(executor): cli = PGCli(pgexecute=executor) @@ -312,6 +335,34 @@ def test_qecho_works(executor): assert result == ["asdf"] +@dbtest +def test_logfile_works(executor): + with tempfile.TemporaryDirectory() as tmpdir: + log_file = f"{tmpdir}/tempfile.log" + cli = PGCli(pgexecute=executor, log_file=log_file) + statement = r"\qecho hello!" + cli.execute_command(statement) + with open(log_file, "r") as f: + log_contents = f.readlines() + assert datetime.datetime.fromisoformat(log_contents[0].strip()) + assert log_contents[1].strip() == r"\qecho hello!" + assert log_contents[2].strip() == "hello!" + + +@dbtest +def test_logfile_unwriteable_file(executor): + cli = PGCli(pgexecute=executor) + statement = r"\log-file forbidden.log" + with mock.patch("builtins.open") as mock_open: + mock_open.side_effect = PermissionError( + "[Errno 13] Permission denied: 'forbidden.log'" + ) + result = run(executor, statement, pgspecial=cli.pgspecial) + assert result == [ + "[Errno 13] Permission denied: 'forbidden.log'\nLogfile capture disabled" + ] + + @dbtest def test_watch_works(executor): cli = PGCli(pgexecute=executor) @@ -431,6 +482,7 @@ def test_pg_service_file(tmpdir): "b_host", "5435", "", + notify_callback, application_name="pgcli", ) del os.environ["PGPASSWORD"] @@ -486,5 +538,50 @@ def test_application_name_db_uri(tmpdir): cli = PGCli(pgclirc_file=str(tmpdir.join("rcfile"))) cli.connect_uri("postgres://bar@baz.com/?application_name=cow") mock_pgexecute.assert_called_with( - "bar", "bar", "", "baz.com", "", "", application_name="cow" + "bar", "bar", "", "baz.com", "", "", notify_callback, application_name="cow" ) + + +@pytest.mark.parametrize( + "duration_in_seconds,words", + [ + (0, "0 seconds"), + (0.0009, "0.001 second"), + (0.0005, "0.001 second"), + (0.0004, "0.0 second"), # not perfect, but will do + (0.2, "0.2 second"), + (1, "1 second"), + (1.4, "1 second"), + (2, "2 seconds"), + (3.4, "3 seconds"), + (60, "1 minute"), + (61, "1 minute 1 second"), + (123, "2 minutes 3 seconds"), + (3600, "1 hour"), + (7235, "2 hours 35 seconds"), + (9005, "2 hours 30 minutes 5 seconds"), + (86401, "24 hours 1 second"), + ], +) +def test_duration_in_words(duration_in_seconds, words): + assert duration_in_words(duration_in_seconds) == words + + +@dbtest +def test_notifications(executor): + run(executor, "listen chan1") + + with mock.patch("pgcli.main.click.secho") as mock_secho: + run(executor, "notify chan1, 'testing1'") + mock_secho.assert_called() + arg = mock_secho.call_args_list[0].args[0] + assert re.match( + r'Notification received on channel "chan1" \(PID \d+\):\ntesting1', + arg, + ) + + run(executor, "unlisten chan1") + + with mock.patch("pgcli.main.click.secho") as mock_secho: + run(executor, "notify chan1, 'testing2'") + mock_secho.assert_not_called() diff --git a/tests/test_pgexecute.py b/tests/test_pgexecute.py index 636795b..f1cadfd 100644 --- a/tests/test_pgexecute.py +++ b/tests/test_pgexecute.py @@ -1,3 +1,4 @@ +import re from textwrap import dedent import psycopg @@ -6,7 +7,7 @@ from unittest.mock import patch, MagicMock from pgspecial.main import PGSpecial, NO_QUERY from utils import run, dbtest, requires_json, requires_jsonb -from pgcli.main import PGCli +from pgcli.main import PGCli, exception_formatter as main_exception_formatter from pgcli.packages.parseutils.meta import FunctionMetadata @@ -219,8 +220,33 @@ def test_database_list(executor): @dbtest def test_invalid_syntax(executor, exception_formatter): - result = run(executor, "invalid syntax!", exception_formatter=exception_formatter) + result = run( + executor, + "invalid syntax!", + exception_formatter=lambda x: main_exception_formatter(x, verbose_errors=False), + ) assert 'syntax error at or near "invalid"' in result[0] + assert "SQLSTATE" not in result[0] + + +@dbtest +def test_invalid_syntax_verbose(executor): + result = run( + executor, + "invalid syntax!", + exception_formatter=lambda x: main_exception_formatter(x, verbose_errors=True), + ) + fields = r""" +Severity: ERROR +Severity \(non-localized\): ERROR +SQLSTATE code: 42601 +Message: syntax error at or near "invalid" +Position: 1 +File: scan\.l +Line: \d+ +Routine: scanner_yyerror + """.strip() + assert re.search(fields, result[0]) @dbtest @@ -690,6 +716,38 @@ def test_function_definition(executor): result = executor.function_definition("the_number_three") +@dbtest +def test_function_notice_order(executor): + run( + executor, + """ + CREATE OR REPLACE FUNCTION demo_order() RETURNS VOID AS + $$ + BEGIN + RAISE NOTICE 'first'; + RAISE NOTICE 'second'; + RAISE NOTICE 'third'; + RAISE NOTICE 'fourth'; + RAISE NOTICE 'fifth'; + RAISE NOTICE 'sixth'; + END; + $$ + LANGUAGE plpgsql; + """, + ) + + executor.function_definition("demo_order") + + result = run(executor, "select demo_order()") + assert "first\nsecond\nthird\nfourth\nfifth\nsixth" in result[0] + assert "+------------+" in result[1] + assert "| demo_order |" in result[2] + assert "|------------|" in result[3] + assert "| |" in result[4] + assert "+------------+" in result[5] + assert "SELECT 1" in result[6] + + @dbtest def test_view_definition(executor): run(executor, "create table tbl1 (a text, b numeric)") @@ -721,6 +779,10 @@ def test_short_host(executor): executor, "host", "localhost1.example.org,localhost2.example.org" ): assert executor.short_host == "localhost1" + with patch.object(executor, "host", "ec2-11-222-333-444.compute-1.amazonaws.com"): + assert executor.short_host == "ec2-11-222-333-444" + with patch.object(executor, "host", "1.2.3.4"): + assert executor.short_host == "1.2.3.4" class VirtualCursor: diff --git a/tests/test_ssh_tunnel.py b/tests/test_ssh_tunnel.py index ae865f4..983212b 100644 --- a/tests/test_ssh_tunnel.py +++ b/tests/test_ssh_tunnel.py @@ -6,7 +6,7 @@ from configobj import ConfigObj from click.testing import CliRunner from sshtunnel import SSHTunnelForwarder -from pgcli.main import cli, PGCli +from pgcli.main import cli, notify_callback, PGCli from pgcli.pgexecute import PGExecute @@ -61,6 +61,7 @@ def test_ssh_tunnel( "127.0.0.1", pgcli.ssh_tunnel.local_bind_ports[0], "", + notify_callback, ) mock_ssh_tunnel_forwarder.reset_mock() mock_pgexecute.reset_mock() @@ -96,6 +97,7 @@ def test_ssh_tunnel( "127.0.0.1", pgcli.ssh_tunnel.local_bind_ports[0], "", + notify_callback, ) mock_ssh_tunnel_forwarder.reset_mock() mock_pgexecute.reset_mock() -- cgit v1.2.3