From 38cb6f125763ac6a648e88c22493efb5c861d45d Mon Sep 17 00:00:00 2001 From: Nicola Coretti Date: Wed, 19 Jun 2024 09:58:54 +0200 Subject: [PATCH] Convert various examples to test cases (#135) * Add tests for tls * Add tests for edge cases * Fix/update tls tests * Add tests for snapshot mode * Add tests for json support --------- Co-authored-by: Mikhail Beck --- noxfile.py | 1 + pyproject.toml | 6 +- test/integration/conftest.py | 58 ++++++++++++ test/integration/edge_cases_test.py | 82 +++++++++++++++++ test/integration/json_test.py | 96 ++++++------------- test/integration/misc_test.py | 138 ++++++++++++++++++++++++++++ test/integration/tls_test.py | 88 ++++++++++++++++++ 7 files changed, 401 insertions(+), 68 deletions(-) create mode 100644 test/integration/edge_cases_test.py create mode 100644 test/integration/misc_test.py create mode 100644 test/integration/tls_test.py diff --git a/noxfile.py b/noxfile.py index 976fae0..1b7de0c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,6 +46,7 @@ def start_db(): session.run( "itde", "spawn-test-environment", + "--create-certificates", "--environment-name", "test", "--database-port-forward", diff --git a/pyproject.toml b/pyproject.toml index 0eb011e..5aa8d09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] +xfail_strict = true markers = [ "smoke: smoke tests which always should pass.", "basic: basic driver tests.", @@ -83,6 +84,9 @@ markers = [ "metadata: tests related to metadata retrieval with pyexasol.", "json: tests related to json serialization in pyexasol.", "dbapi2: tests related to dbapi2 compatibility.", - "configuration: tests related to pyexasol settings and configuration." + "configuration: tests related to pyexasol settings and configuration.", + "edge_cases: tests related to pyexasol and exasol edge cases scenarios.", + "tls: tests related to tls.", + "misc: miscellaneous tests which did not fit in the other categories." ] diff --git a/test/integration/conftest.py b/test/integration/conftest.py index f01e815..5f91a0a 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -1,8 +1,10 @@ import os import uuid import pytest +import decimal import pyexasol import subprocess +from inspect import cleandoc from pathlib import Path import logging @@ -69,6 +71,62 @@ def prepare_database(dsn, user, password): loader.load() +@pytest.fixture +def edge_case_ddl(): + table_name = "edge_case" + ddl = cleandoc( + f"""CREATE OR REPLACE TABLE {table_name} + ( + dec36_0 DECIMAL(36,0), + dec36_36 DECIMAL(36,36), + dbl DOUBLE, + bl BOOLEAN, + dt DATE, + ts TIMESTAMP, + var100 VARCHAR(100), + var2000000 VARCHAR(2000000) + ) + """ + ) + yield table_name, ddl + + +@pytest.fixture +def edge_cases(): + return { + "Biggest-Values": { + "DEC36_0": decimal.Decimal("+" + ("9" * 36)), + "DEC36_36": decimal.Decimal("+0." + ("9" * 36)), + "DBL": 1.7e308, + "BL": True, + "DT": "9999-12-31", + "TS": "9999-12-31 23:59:59.999", + "VAR100": "ひ" * 100, + "VAR2000000": "ひ" * 2000000, + }, + "Smallest-Values": { + "DEC36_0": decimal.Decimal("-" + ("9" * 36)), + "DEC36_36": decimal.Decimal("-0." + ("9" * 36)), + "DBL": -1.7e308, + "BL": False, + "DT": "0001-01-01", + "TS": "0001-01-01 00:00:00", + "VAR100": "", + "VAR2000000": "ひ", + }, + "All-Nulls": { + "DEC36_0": None, + "DEC36_36": None, + "DBL": None, + "BL": None, + "DT": None, + "TS": None, + "VAR100": None, + "VAR2000000": None, + }, + } + + class DockerDataLoader: """Data loader for docker based Exasol DB""" diff --git a/test/integration/edge_cases_test.py b/test/integration/edge_cases_test.py new file mode 100644 index 0000000..2ef739d --- /dev/null +++ b/test/integration/edge_cases_test.py @@ -0,0 +1,82 @@ +import pytest + + +@pytest.fixture +def empty_table(connection, edge_case_ddl): + table_name, ddl = edge_case_ddl + connection.execute(ddl) + connection.commit() + + yield table_name + + delete_stmt = f"DROP TABLE IF EXISTS {table_name};" + connection.execute(delete_stmt) + connection.commit() + + +@pytest.fixture +def poplulate_edge_case_table(connection, empty_table, edge_cases): + table = "edge_case" + stmt = ( + f"INSERT INTO {table} VALUES ({{DEC36_0!d}}, {{DEC36_36!d}}, {{DBL!f}}, " + "{BL}, {DT}, {TS}, {VAR100}, {VAR2000000})" + ) + for case in edge_cases.values(): + connection.execute(stmt, dict(case)) + + connection.commit() + + yield table + + +@pytest.mark.edge_case +def test_insert(connection, empty_table, edge_cases): + stmt = ( + f"INSERT INTO {empty_table} VALUES ({{DEC36_0!d}}, {{DEC36_36!d}}, {{DBL!f}}, " + "{BL}, {DT}, {TS}, {VAR100}, {VAR2000000})" + ) + for case in edge_cases.values(): + connection.execute(stmt, dict(case)) + + expected = len(edge_cases) + actual = connection.execute(f"SELECT COUNT(*) FROM {empty_table};").fetchval() + + assert actual == expected + + +@pytest.mark.edge_case +def test_select_and_fetch(connection, edge_cases, poplulate_edge_case_table): + query = ( + "SELECT DEC36_0, DEC36_36, DBL, BL, DT, TS, VAR100, " + "LENGTH(var2000000) AS len_var FROM edge_case" + ) + + result = connection.execute(query).fetchall() + + expected = len(edge_cases.values()) + actual = len(result) + + assert actual == expected + + +@pytest.mark.edge_case +def test_very_long_query(connection, edge_cases): + query = ( + "SELECT {VAL1} AS VAL1, {VAL2} AS VAL2, " + "{VAL3} AS VAL3, {VAL4} AS VAL4, {VAL5} AS VAL5" + ) + case = "Biggest-Values" + params = { + "VAL1": edge_cases[case]["VAR2000000"], + "VAL2": edge_cases[case]["VAR2000000"], + "VAL3": edge_cases[case]["VAR2000000"], + "VAL4": edge_cases[case]["VAR2000000"], + "VAL5": edge_cases[case]["VAR2000000"], + } + + result = connection.execute(query, params) + + expected = 10000065 + actual = len(result.query) + + assert actual == expected diff --git a/test/integration/json_test.py b/test/integration/json_test.py index 61a3821..bdb0e68 100644 --- a/test/integration/json_test.py +++ b/test/integration/json_test.py @@ -1,8 +1,5 @@ import pytest import pyexasol -import decimal - -from inspect import cleandoc @pytest.fixture @@ -27,98 +24,45 @@ def factory(json_lib): @pytest.fixture -def table(connection): - name = "edge_case" - ddl = cleandoc( - f"""CREATE OR REPLACE TABLE {name} - ( - dec36_0 DECIMAL(36,0), - dec36_36 DECIMAL(36,36), - dbl DOUBLE, - bl BOOLEAN, - dt DATE, - ts TIMESTAMP, - var100 VARCHAR(100), - var2000000 VARCHAR(2000000) - ) - """ - ) +def empty_table(connection, edge_case_ddl): + table_name, ddl = edge_case_ddl connection.execute(ddl) connection.commit() - yield name + yield table_name - delete_stmt = f"DROP TABLE IF EXISTS {name};" + delete_stmt = f"DROP TABLE IF EXISTS {table_name};" connection.execute(delete_stmt) connection.commit() -@pytest.fixture -def edge_cases(): - return [ - # Biggest values - { - "DEC36_0": decimal.Decimal("+" + ("9" * 36)), - "DEC36_36": decimal.Decimal("+0." + ("9" * 36)), - "DBL": 1.7e308, - "BL": True, - "DT": "9999-12-31", - "TS": "9999-12-31 23:59:59.999", - "VAR100": "ひ" * 100, - "VAR2000000": "ひ" * 2000000, - }, - # Smallest values - { - "DEC36_0": decimal.Decimal("-" + ("9" * 36)), - "DEC36_36": decimal.Decimal("-0." + ("9" * 36)), - "DBL": -1.7e308, - "BL": False, - "DT": "0001-01-01", - "TS": "0001-01-01 00:00:00", - "VAR100": "", - "VAR2000000": "ひ", - }, - # All nulls - { - "DEC36_0": None, - "DEC36_36": None, - "DBL": None, - "BL": None, - "DT": None, - "TS": None, - "VAR100": None, - "VAR2000000": None, - }, - ] - - @pytest.mark.json @pytest.mark.parametrize("json_lib", ["orjson", "ujson", "rapidjson"]) -def test_insert(table, connection_factory, edge_cases, json_lib): +def test_insert(empty_table, connection_factory, edge_cases, json_lib): connection = connection_factory(json_lib) insert_stmt = ( "INSERT INTO edge_case VALUES" "({DEC36_0!d}, {DEC36_36!d}, {DBL!f}, {BL}, {DT}, {TS}, {VAR100}, {VAR2000000})" ) - for edge_case in edge_cases: + for edge_case in edge_cases.values(): connection.execute(insert_stmt, edge_case) expected = len(edge_cases) - actual = connection.execute(f"SELECT COUNT(*) FROM {table};").fetchval() + actual = connection.execute(f"SELECT COUNT(*) FROM {empty_table};").fetchval() assert actual == expected @pytest.mark.json @pytest.mark.parametrize("json_lib", ["orjson", "ujson", "rapidjson"]) -def test_select(table, connection_factory, edge_cases, json_lib): +def test_select(empty_table, connection_factory, edge_cases, json_lib): connection = connection_factory(json_lib) insert_stmt = ( "INSERT INTO edge_case VALUES" "({DEC36_0!d}, {DEC36_36!d}, {DBL!f}, {BL}, {DT}, {TS}, {VAR100}, {VAR2000000})" ) - for edge_case in edge_cases: + for edge_case in edge_cases.values(): connection.execute(insert_stmt, edge_case) select_stmt = ( @@ -128,9 +72,27 @@ def test_select(table, connection_factory, edge_cases, json_lib): expected = { # Biggest values - ("9" * 36, f"0.{'9' * 36}", 1.7e308, True, "9999-12-31", "9999-12-31 23:59:59.999000", "ひ" * 100, 2000000), + ( + "9" * 36, + f"0.{'9' * 36}", + 1.7e308, + True, + "9999-12-31", + "9999-12-31 23:59:59.999000", + "ひ" * 100, + 2000000, + ), # Smallest values - (f"-{'9' * 36}", f"-0.{'9' * 36}", -1.7e308, False, "0001-01-01", "0001-01-01 00:00:00.000000", None, 1), + ( + f"-{'9' * 36}", + f"-0.{'9' * 36}", + -1.7e308, + False, + "0001-01-01", + "0001-01-01 00:00:00.000000", + None, + 1, + ), # All nulls (None, None, None, None, None, None, None, None), } diff --git a/test/integration/misc_test.py b/test/integration/misc_test.py new file mode 100644 index 0000000..3f4cad8 --- /dev/null +++ b/test/integration/misc_test.py @@ -0,0 +1,138 @@ +import pyexasol +import pytest +from inspect import cleandoc + + +@pytest.fixture +def disable_query_cache_for_session(connection): + query = ( + "SELECT session_value FROM EXA_PARAMETERS " + "WHERE parameter_name = 'QUERY_CACHE';" + ) + stmt = "ALTER SESSION SET QUERY_CACHE = '{mode}'" + mode = connection.execute(query).fetchval() + connection.execute(stmt.format(mode="OFF")) + yield + connection.execute(stmt.format(mode=mode)) + + +@pytest.fixture +def enable_session_profiling(connection): + stmt = "ALTER SESSION SET PROFILE = '{mode}'" + connection.execute(stmt.format(mode="ON")) + yield + connection.execute(stmt.format(mode="OFF")) + + +@pytest.fixture +def disable_snapshots(connection): + query = ( + "SELECT system_value FROM EXA_PARAMETERS " + "WHERE parameter_name = 'SNAPSHOT_MODE';" + ) + mode = connection.execute(query).fetchval() + stmt = "ALTER SYSTEM SET SNAPSHOT_MODE = '{mode}';" + connection.execute(stmt.format(mode="OFF")) + yield + connection.execute(stmt.format(mode=mode)) + + +@pytest.fixture +def query(): + # fmt: off + yield cleandoc( + """ + SELECT u.user_id, sum(p.gross_amt) AS total_gross_amt + FROM users u + LEFT JOIN payments p ON (u.user_id=p.user_id) + GROUP BY 1 + ORDER BY 2 DESC NULLS LAST + LIMIT 10 + """ + ) + # fmt: on + + +@pytest.mark.misc +def test_snapshot_mode(disable_snapshots, dsn, user, password, schema): + query = ( + "SELECT session_value FROM EXA_PARAMETERS " + "WHERE parameter_name = 'SNAPSHOT_MODE';" + ) + + with pyexasol.connect(dsn=dsn, user=user, password=password, schema=schema) as con: + mode = con.execute(query).fetchval() + + expected = "OFF" + actual = mode + + assert actual == expected + + with pyexasol.connect( + dsn=dsn, user=user, password=password, schema=schema, snapshot_transactions=True + ) as con: + mode = con.execute(query).fetchval() + + expected = "SYSTEM TABLES" + actual = mode + + assert actual == expected + + +@pytest.mark.misc +def test_normal_profiling( + connection, disable_query_cache_for_session, enable_session_profiling, query +): + expected = { + "cpu", + "duration", + "hdd_read", + "hdd_write", + "in_rows", + "mem_peak", + "net", + "object_name", + "object_rows", + "object_schema", + "out_rows", + "part_id", + "part_info", + "part_name", + "remarks", + "start_time", + "stop_time", + "temp_db_ram_peak", + } + result = connection.ext.explain_last() + actual = set(result[0]) + assert actual == expected + + +@pytest.mark.misc +def test_detailed_profiling( + connection, disable_query_cache_for_session, enable_session_profiling, query +): + expected = { + "cpu", + "duration", + "hdd_read", + "hdd_write", + "in_rows", + "iproc", + "mem_peak", + "net", + "object_name", + "object_rows", + "object_schema", + "out_rows", + "part_id", + "part_info", + "part_name", + "remarks", + "start_time", + "stop_time", + "temp_db_ram_peak", + } + result = connection.ext.explain_last(details=True) + actual = set(result[0]) + assert actual == expected diff --git a/test/integration/tls_test.py b/test/integration/tls_test.py new file mode 100644 index 0000000..cff2b1b --- /dev/null +++ b/test/integration/tls_test.py @@ -0,0 +1,88 @@ +import hashlib +import pytest +import pyexasol +from pyexasol import ExaConnectionFailedError + + +@pytest.fixture +def connection(dsn, user, password, schema): + con = pyexasol.connect( + dsn=dsn, + user=user, + password=password, + schema=schema, + ) + yield con + con.close() + + +@pytest.fixture +def server_fingerprint(connection): + cert = connection._ws.sock.getpeercert(True) + fingerprint = hashlib.sha256(cert).hexdigest().upper() + yield fingerprint + + +def _dsn_with_fingerprint(dsn: str, fingerprint: str): + if ":" in dsn: + dsn = dsn.replace(":", f"/{fingerprint}:") + else: + dsn = f"{dsn}/{fingerprint}" + return dsn + + +@pytest.fixture +def dsn_with_valid_fingerprint(dsn, server_fingerprint): + yield _dsn_with_fingerprint(dsn, server_fingerprint) + + +@pytest.fixture +def dsn_with_invalid_fingerprint(dsn): + yield _dsn_with_fingerprint(dsn, "123abc") + + +@pytest.mark.xfail( + reason="For futher details see: https://github.com/exasol/integration-tasks/issues/512" +) +@pytest.mark.tls +def test_connect_fails_due_to_strict_certificate_validation_by_default(): + assert False + + +@pytest.mark.tls +def test_connect_with_tls(dsn, user, password, schema): + expected = 1 + with pyexasol.connect( + dsn=dsn, user=user, password=password, schema=schema, encryption=True + ) as connection: + actual = connection.execute("SELECT 1;").fetchval() + + assert actual == expected + + +@pytest.mark.tls +def test_connect_with_valid_fingerprint( + dsn_with_valid_fingerprint, user, password, schema +): + dsn = dsn_with_valid_fingerprint + expected = 1 + with pyexasol.connect( + dsn=dsn, user=user, password=password, schema=schema, encryption=True + ) as connection: + actual = connection.execute("SELECT 1;").fetchval() + + assert actual == expected + + +@pytest.mark.tls +def test_connect_with_invalid_fingerprint_fails( + dsn_with_invalid_fingerprint, user, password, schema +): + dsn = dsn_with_invalid_fingerprint + with pytest.raises(ExaConnectionFailedError) as exec_info: + pyexasol.connect( + dsn=dsn, user=user, password=password, schema=schema, encryption=True + ) + + expected = "did not match server fingerprint" + assert expected in str(exec_info.value)