diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 83c7e0ff..afb5a5b2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: matrix: os: ['ubuntu-22.04', 'macos-latest'] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] - cratedb-version: ['5.8.3'] + cratedb-version: ['5.9.2'] # To save resources, only verify the most recent Python versions on macOS. exclude: diff --git a/CHANGES.txt b/CHANGES.txt index 4a0f0a48..aac5886b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,10 @@ Unreleased "Threads may share the module, but not connections." - Added ``error_trace`` to string representation of an Error to relay server stacktraces into exception messages. +- Refactoring: The module namespace ``crate.client.test_util`` has been + renamed to ``crate.testing.util``. +- Added ``BulkResponse`` wrapper for improved decoding of CrateDB HTTP bulk + responses including ``rowcount=`` items. .. _Migrate from crate.client to sqlalchemy-cratedb: https://cratedb.com/docs/sqlalchemy-cratedb/migrate-from-crate-client.html .. _sqlalchemy-cratedb: https://pypi.org/project/sqlalchemy-cratedb/ diff --git a/DEVELOP.rst b/DEVELOP.rst index 41373f18..b523a4bf 100644 --- a/DEVELOP.rst +++ b/DEVELOP.rst @@ -26,34 +26,40 @@ see, for example, `useful command-line options for zope-testrunner`_. Run all tests:: - ./bin/test -vvvv + bin/test Run specific tests:: - ./bin/test -vvvv -t test_score + # Select modules. + bin/test -t test_cursor + bin/test -t client + bin/test -t testing + + # Select doctests. + bin/test -t http.rst Ignore specific test directories:: - ./bin/test -vvvv --ignore_dir=testing + bin/test --ignore_dir=testing The ``LayerTest`` test cases have quite some overhead. Omitting them will save a few cycles (~70 seconds runtime):: - ./bin/test -t '!LayerTest' + bin/test -t '!LayerTest' -Invoke all tests without integration tests (~15 seconds runtime):: +Invoke all tests without integration tests (~10 seconds runtime):: - ./bin/test --layer '!crate.testing.layer.crate' --test '!LayerTest' + bin/test --layer '!crate.testing.layer.crate' --test '!LayerTest' -Yet ~130 test cases, but only ~5 seconds runtime:: +Yet ~60 test cases, but only ~1 second runtime:: - ./bin/test --layer '!crate.testing.layer.crate' --test '!LayerTest' \ + bin/test --layer '!crate.testing.layer.crate' --test '!LayerTest' \ -t '!test_client_threaded' -t '!test_no_retry_on_read_timeout' \ -t '!test_wait_for_http' -t '!test_table_clustered_by' To inspect the whole list of test cases, run:: - ./bin/test --list-tests + bin/test --list-tests You can run the tests against multiple Python interpreters with `tox`_:: diff --git a/bin/test b/bin/test index 05407417..749ec64b 100755 --- a/bin/test +++ b/bin/test @@ -12,6 +12,6 @@ sys.argv[0] = os.path.abspath(sys.argv[0]) if __name__ == '__main__': zope.testrunner.run([ - '-vvv', '--auto-color', - '--test-path', join(base, 'src')], - ) + '-vvvv', '--auto-color', + '--path', join(base, 'tests'), + ]) diff --git a/bootstrap.sh b/bootstrap.sh index 9e011195..06c52f12 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -17,7 +17,7 @@ # set -x # Default variables. -CRATEDB_VERSION=${CRATEDB_VERSION:-5.8.3} +CRATEDB_VERSION=${CRATEDB_VERSION:-5.9.2} function print_header() { diff --git a/docs/by-example/connection.rst b/docs/by-example/connection.rst index 4b89db7d..108166a3 100644 --- a/docs/by-example/connection.rst +++ b/docs/by-example/connection.rst @@ -21,7 +21,7 @@ connect() This section sets up a connection object, and inspects some of its attributes. >>> from crate.client import connect - >>> from crate.client.test_util import ClientMocked + >>> from crate.testing.util import ClientMocked >>> connection = connect(client=ClientMocked()) >>> connection.lowest_server_version.version diff --git a/docs/by-example/cursor.rst b/docs/by-example/cursor.rst index 7fc7da7d..c649ee8c 100644 --- a/docs/by-example/cursor.rst +++ b/docs/by-example/cursor.rst @@ -23,7 +23,7 @@ up the response for subsequent cursor operations. >>> from crate.client import connect >>> from crate.client.converter import DefaultTypeConverter >>> from crate.client.cursor import Cursor - >>> from crate.client.test_util import ClientMocked + >>> from crate.testing.util import ClientMocked >>> connection = connect(client=ClientMocked()) >>> cursor = connection.cursor() diff --git a/src/crate/client/result.py b/src/crate/client/result.py new file mode 100644 index 00000000..ed8f069d --- /dev/null +++ b/src/crate/client/result.py @@ -0,0 +1,68 @@ +import typing as t +from functools import cached_property + + +class BulkResultItem(t.TypedDict): + """ + Define the shape of a CrateDB bulk request response item. + """ + + rowcount: int + + +class BulkResponse: + """ + Manage a response to a CrateDB bulk request. + Accepts a list of bulk arguments (parameter list) and a list of bulk response items. + + https://cratedb.com/docs/crate/reference/en/latest/interfaces/http.html#bulk-operations + """ + + def __init__( + self, + records: t.List[t.Dict[str, t.Any]], + results: t.List[BulkResultItem]): + if records is None: + raise ValueError("Processing a bulk response without records is an invalid operation") + if results is None: + raise ValueError("Processing a bulk response without results is an invalid operation") + self.records = records + self.results = results + + @cached_property + def failed_records(self) -> t.List[t.Dict[str, t.Any]]: + """ + Compute list of failed records. + + CrateDB signals failed inserts using `rowcount=-2`. + + https://cratedb.com/docs/crate/reference/en/latest/interfaces/http.html#error-handling + """ + errors: t.List[t.Dict[str, t.Any]] = [] + for record, status in zip(self.records, self.results): + if status["rowcount"] == -2: + errors.append(record) + return errors + + @cached_property + def record_count(self) -> int: + """ + Compute bulk size / length of parameter list. + """ + if not self.records: + return 0 + return len(self.records) + + @cached_property + def success_count(self) -> int: + """ + Compute number of succeeding records within a batch. + """ + return self.record_count - self.failed_count + + @cached_property + def failed_count(self) -> int: + """ + Compute number of failed records within a batch. + """ + return len(self.failed_records) diff --git a/src/crate/client/test_util.py b/src/crate/client/test_util.py deleted file mode 100644 index 823a44e3..00000000 --- a/src/crate/client/test_util.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8; -*- -# -# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor -# license agreements. See the NOTICE file distributed with this work for -# additional information regarding copyright ownership. Crate licenses -# this file to you under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# However, if you have executed another commercial license agreement -# with Crate these terms will supersede the license and you may use the -# software solely pursuant to the terms of the relevant commercial agreement. -import unittest - - -class ClientMocked(object): - - active_servers = ["http://localhost:4200"] - - def __init__(self): - self.response = {} - self._server_infos = ("http://localhost:4200", "my server", "2.0.0") - - def sql(self, stmt=None, parameters=None, bulk_parameters=None): - return self.response - - def server_infos(self, server): - return self._server_infos - - def set_next_response(self, response): - self.response = response - - def set_next_server_infos(self, server, server_name, version): - self._server_infos = (server, server_name, version) - - def close(self): - pass - - -class ParametrizedTestCase(unittest.TestCase): - """ - TestCase classes that want to be parametrized should - inherit from this class. - - https://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases - """ - def __init__(self, methodName="runTest", param=None): - super(ParametrizedTestCase, self).__init__(methodName) - self.param = param - - @staticmethod - def parametrize(testcase_klass, param=None): - """ Create a suite containing all tests taken from the given - subclass, passing them the parameter 'param'. - """ - testloader = unittest.TestLoader() - testnames = testloader.getTestCaseNames(testcase_klass) - suite = unittest.TestSuite() - for name in testnames: - suite.addTest(testcase_klass(name, param=param)) - return suite diff --git a/src/crate/testing/util.py b/src/crate/testing/util.py index 3e9885d6..54f9098c 100644 --- a/src/crate/testing/util.py +++ b/src/crate/testing/util.py @@ -1,3 +1,74 @@ +# -*- coding: utf-8; -*- +# +# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. Crate licenses +# this file to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# However, if you have executed another commercial license agreement +# with Crate these terms will supersede the license and you may use the +# software solely pursuant to the terms of the relevant commercial agreement. +import unittest + + +class ClientMocked(object): + + active_servers = ["http://localhost:4200"] + + def __init__(self): + self.response = {} + self._server_infos = ("http://localhost:4200", "my server", "2.0.0") + + def sql(self, stmt=None, parameters=None, bulk_parameters=None): + return self.response + + def server_infos(self, server): + return self._server_infos + + def set_next_response(self, response): + self.response = response + + def set_next_server_infos(self, server, server_name, version): + self._server_infos = (server, server_name, version) + + def close(self): + pass + + +class ParametrizedTestCase(unittest.TestCase): + """ + TestCase classes that want to be parametrized should + inherit from this class. + + https://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases + """ + def __init__(self, methodName="runTest", param=None): + super(ParametrizedTestCase, self).__init__(methodName) + self.param = param + + @staticmethod + def parametrize(testcase_klass, param=None): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + testloader = unittest.TestLoader() + testnames = testloader.getTestCaseNames(testcase_klass) + suite = unittest.TestSuite() + for name in testnames: + suite.addTest(testcase_klass(name, param=param)) + return suite + + class ExtraAssertions: """ Additional assert methods for unittest. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/crate/testing/testdata/data/test_a.json b/tests/assets/import/test_a.json similarity index 100% rename from src/crate/testing/testdata/data/test_a.json rename to tests/assets/import/test_a.json diff --git a/src/crate/testing/testdata/mappings/locations.sql b/tests/assets/mappings/locations.sql similarity index 100% rename from src/crate/testing/testdata/mappings/locations.sql rename to tests/assets/mappings/locations.sql diff --git a/src/crate/client/pki/cacert_invalid.pem b/tests/assets/pki/cacert_invalid.pem similarity index 100% rename from src/crate/client/pki/cacert_invalid.pem rename to tests/assets/pki/cacert_invalid.pem diff --git a/src/crate/client/pki/cacert_valid.pem b/tests/assets/pki/cacert_valid.pem similarity index 100% rename from src/crate/client/pki/cacert_valid.pem rename to tests/assets/pki/cacert_valid.pem diff --git a/src/crate/client/pki/client_invalid.pem b/tests/assets/pki/client_invalid.pem similarity index 100% rename from src/crate/client/pki/client_invalid.pem rename to tests/assets/pki/client_invalid.pem diff --git a/src/crate/client/pki/client_valid.pem b/tests/assets/pki/client_valid.pem similarity index 100% rename from src/crate/client/pki/client_valid.pem rename to tests/assets/pki/client_valid.pem diff --git a/src/crate/client/pki/readme.rst b/tests/assets/pki/readme.rst similarity index 100% rename from src/crate/client/pki/readme.rst rename to tests/assets/pki/readme.rst diff --git a/src/crate/client/pki/server_valid.pem b/tests/assets/pki/server_valid.pem similarity index 100% rename from src/crate/client/pki/server_valid.pem rename to tests/assets/pki/server_valid.pem diff --git a/src/crate/testing/testdata/settings/test_a.json b/tests/assets/settings/test_a.json similarity index 100% rename from src/crate/testing/testdata/settings/test_a.json rename to tests/assets/settings/test_a.json diff --git a/tests/client/__init__.py b/tests/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/crate/client/tests.py b/tests/client/layer.py similarity index 71% rename from src/crate/client/tests.py rename to tests/client/layer.py index 2f6be428..b2d521e7 100644 --- a/src/crate/client/tests.py +++ b/tests/client/layer.py @@ -25,7 +25,6 @@ import os import socket import unittest -import doctest from pprint import pprint from http.server import HTTPServer, BaseHTTPRequestHandler import ssl @@ -35,25 +34,12 @@ import stopit -from crate.testing.layer import CrateLayer -from crate.testing.settings import \ - crate_host, crate_path, crate_port, \ - crate_transport_port, docs_path, localhost from crate.client import connect +from crate.testing.layer import CrateLayer +from .settings import \ + assets_path, crate_host, crate_path, crate_port, \ + crate_transport_port, localhost -from .test_cursor import CursorTest -from .test_connection import ConnectionTest -from .test_http import ( - HttpClientTest, - ThreadSafeHttpClientTest, - KeepAliveClientTest, - ParamsTest, - RetryOnTimeoutServerTest, - RequestsCaBundleTest, - TestUsernameSentAsHeader, - TestCrateJsonEncoder, - TestDefaultSchemaHeader, -) makeSuite = unittest.TestLoader().loadTestsFromTestCase @@ -110,14 +96,15 @@ def ensure_cratedb_layer(): def setUpCrateLayerBaseline(test): - test.globs['crate_host'] = crate_host - test.globs['pprint'] = pprint - test.globs['print'] = cprint + if hasattr(test, "globs"): + test.globs['crate_host'] = crate_host + test.globs['pprint'] = pprint + test.globs['print'] = cprint with connect(crate_host) as conn: cursor = conn.cursor() - with open(docs_path('testing/testdata/mappings/locations.sql')) as s: + with open(assets_path('mappings/locations.sql')) as s: stmt = s.read() cursor.execute(stmt) stmt = ("select count(*) from information_schema.tables " @@ -125,7 +112,7 @@ def setUpCrateLayerBaseline(test): cursor.execute(stmt) assert cursor.fetchall()[0][0] == 1 - data_path = docs_path('testing/testdata/data/test_a.json') + data_path = assets_path('import/test_a.json') # load testing data into crate cursor.execute("copy locations from ?", (data_path,)) # refresh location table so imported data is visible immediately @@ -146,6 +133,7 @@ def tearDownDropEntitiesBaseline(test): Drop all tables, views, and users created by `setUpWithCrateLayer*`. """ ddl_statements = [ + "DROP TABLE foobar", "DROP TABLE locations", "DROP BLOB TABLE myfiles", "DROP USER me", @@ -157,10 +145,8 @@ def tearDownDropEntitiesBaseline(test): class HttpsTestServerLayer: PORT = 65534 HOST = "localhost" - CERT_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - "pki/server_valid.pem")) - CACERT_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - "pki/cacert_valid.pem")) + CERT_FILE = assets_path("pki/server_valid.pem") + CACERT_FILE = assets_path("pki/cacert_valid.pem") __name__ = "httpsserver" __bases__ = tuple() @@ -249,18 +235,10 @@ def setUpWithHttps(test): test.globs['pprint'] = pprint test.globs['print'] = cprint - test.globs['cacert_valid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/cacert_valid.pem") - ) - test.globs['cacert_invalid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/cacert_invalid.pem") - ) - test.globs['clientcert_valid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/client_valid.pem") - ) - test.globs['clientcert_invalid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/client_invalid.pem") - ) + test.globs['cacert_valid'] = assets_path("pki/cacert_valid.pem") + test.globs['cacert_invalid'] = assets_path("pki/cacert_invalid.pem") + test.globs['clientcert_valid'] = assets_path("pki/client_valid.pem") + test.globs['clientcert_invalid'] = assets_path("pki/client_invalid.pem") def _execute_statements(statements, on_error="ignore"): @@ -283,58 +261,3 @@ def _execute_statement(cursor, stmt, on_error="ignore"): pass elif on_error == "raise": raise - - -def test_suite(): - suite = unittest.TestSuite() - flags = (doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) - - # Unit tests. - suite.addTest(makeSuite(CursorTest)) - suite.addTest(makeSuite(HttpClientTest)) - suite.addTest(makeSuite(KeepAliveClientTest)) - suite.addTest(makeSuite(ThreadSafeHttpClientTest)) - suite.addTest(makeSuite(ParamsTest)) - suite.addTest(makeSuite(ConnectionTest)) - suite.addTest(makeSuite(RetryOnTimeoutServerTest)) - suite.addTest(makeSuite(RequestsCaBundleTest)) - suite.addTest(makeSuite(TestUsernameSentAsHeader)) - suite.addTest(makeSuite(TestCrateJsonEncoder)) - suite.addTest(makeSuite(TestDefaultSchemaHeader)) - suite.addTest(doctest.DocTestSuite('crate.client.connection')) - suite.addTest(doctest.DocTestSuite('crate.client.http')) - - s = doctest.DocFileSuite( - 'docs/by-example/connection.rst', - 'docs/by-example/cursor.rst', - module_relative=False, - optionflags=flags, - encoding='utf-8' - ) - suite.addTest(s) - - s = doctest.DocFileSuite( - 'docs/by-example/https.rst', - module_relative=False, - setUp=setUpWithHttps, - optionflags=flags, - encoding='utf-8' - ) - s.layer = HttpsTestServerLayer() - suite.addTest(s) - - # Integration tests. - s = doctest.DocFileSuite( - 'docs/by-example/http.rst', - 'docs/by-example/client.rst', - 'docs/by-example/blob.rst', - module_relative=False, - setUp=setUpCrateLayerBaseline, - tearDown=tearDownDropEntitiesBaseline, - optionflags=flags, - encoding='utf-8' - ) - s.layer = ensure_cratedb_layer() - suite.addTest(s) - - return suite diff --git a/src/crate/testing/settings.py b/tests/client/settings.py similarity index 77% rename from src/crate/testing/settings.py rename to tests/client/settings.py index 34793cc6..228222fd 100644 --- a/src/crate/testing/settings.py +++ b/tests/client/settings.py @@ -21,27 +21,20 @@ # software solely pursuant to the terms of the relevant commercial agreement. from __future__ import absolute_import -import os +from pathlib import Path -def docs_path(*parts): - return os.path.abspath( - os.path.join( - os.path.dirname(os.path.dirname(__file__)), *parts - ) - ) +def assets_path(*parts) -> str: + return str((project_root() / "tests" / "assets").joinpath(*parts).absolute()) -def project_root(*parts): - return os.path.abspath( - os.path.join(docs_path("..", ".."), *parts) - ) +def crate_path() -> str: + return str(project_root() / "parts" / "crate") -def crate_path(*parts): - return os.path.abspath( - project_root("parts", "crate", *parts) - ) +def project_root() -> Path: + return Path(__file__).parent.parent.parent + crate_port = 44209 diff --git a/src/crate/client/test_connection.py b/tests/client/test_connection.py similarity index 96% rename from src/crate/client/test_connection.py rename to tests/client/test_connection.py index 93510864..5badfab2 100644 --- a/src/crate/client/test_connection.py +++ b/tests/client/test_connection.py @@ -2,12 +2,12 @@ from urllib3 import Timeout -from .connection import Connection -from .http import Client +from crate.client.connection import Connection +from crate.client.http import Client from crate.client import connect from unittest import TestCase -from ..testing.settings import crate_host +from .settings import crate_host class ConnectionTest(TestCase): diff --git a/src/crate/client/test_cursor.py b/tests/client/test_cursor.py similarity index 99% rename from src/crate/client/test_cursor.py rename to tests/client/test_cursor.py index 79e7ddd6..318c172b 100644 --- a/src/crate/client/test_cursor.py +++ b/tests/client/test_cursor.py @@ -33,7 +33,7 @@ from crate.client import connect from crate.client.converter import DataType, DefaultTypeConverter from crate.client.http import Client -from crate.client.test_util import ClientMocked +from crate.testing.util import ClientMocked class CursorTest(TestCase): diff --git a/src/crate/client/test_exceptions.py b/tests/client/test_exceptions.py similarity index 100% rename from src/crate/client/test_exceptions.py rename to tests/client/test_exceptions.py diff --git a/src/crate/client/test_http.py b/tests/client/test_http.py similarity index 98% rename from src/crate/client/test_http.py rename to tests/client/test_http.py index 8e547963..fd538fc1 100644 --- a/src/crate/client/test_http.py +++ b/tests/client/test_http.py @@ -43,8 +43,8 @@ import uuid import certifi -from .http import Client, CrateJsonEncoder, _get_socket_opts, _remove_certs_for_non_https -from .exceptions import ConnectionError, ProgrammingError, IntegrityError +from crate.client.http import Client, CrateJsonEncoder, _get_socket_opts, _remove_certs_for_non_https +from crate.client.exceptions import ConnectionError, ProgrammingError, IntegrityError REQUEST = 'crate.client.http.Server.request' CA_CERT_PATH = certifi.where() @@ -127,7 +127,7 @@ def test_connection_reset_exception(self): client.close() def test_no_connection_exception(self): - client = Client() + client = Client(servers="localhost:9999") self.assertRaises(ConnectionError, client.sql, 'select foo') client.close() diff --git a/tests/client/test_result.py b/tests/client/test_result.py new file mode 100644 index 00000000..dfed504f --- /dev/null +++ b/tests/client/test_result.py @@ -0,0 +1,88 @@ +import sys +import unittest + +from crate import client +from crate.client.exceptions import ProgrammingError +from .layer import setUpCrateLayerBaseline, tearDownDropEntitiesBaseline +from .settings import crate_host + + +class BulkOperationTest(unittest.TestCase): + + def setUp(self): + setUpCrateLayerBaseline(self) + + def tearDown(self): + tearDownDropEntitiesBaseline(self) + + @unittest.skipIf(sys.version_info < (3, 8), "BulkResponse needs Python 3.8 or higher") + def test_executemany_with_bulk_response_partial(self): + + # Import at runtime is on purpose, to permit skipping the test case. + from crate.client.result import BulkResponse + + connection = client.connect(crate_host) + cursor = connection.cursor() + + # Run SQL DDL. + cursor.execute("CREATE TABLE foobar (id INTEGER PRIMARY KEY, name STRING);") + + # Run a batch insert that only partially succeeds. + invalid_records = [(1, "Hotzenplotz 1"), (1, "Hotzenplotz 2")] + result = cursor.executemany("INSERT INTO foobar (id, name) VALUES (?, ?)", invalid_records) + + # Verify CrateDB response. + self.assertEqual(result, [{"rowcount": 1}, {"rowcount": -2}]) + + # Verify decoded response. + bulk_response = BulkResponse(invalid_records, result) + self.assertEqual(bulk_response.failed_records, [(1, "Hotzenplotz 2")]) + self.assertEqual(bulk_response.record_count, 2) + self.assertEqual(bulk_response.success_count, 1) + self.assertEqual(bulk_response.failed_count, 1) + + cursor.execute("REFRESH TABLE foobar;") + cursor.execute("SELECT * FROM foobar;") + result = cursor.fetchall() + self.assertEqual(result, [[1, "Hotzenplotz 1"]]) + + cursor.close() + connection.close() + + @unittest.skipIf(sys.version_info < (3, 8), "BulkResponse needs Python 3.8 or higher") + def test_executemany_empty(self): + + connection = client.connect(crate_host) + cursor = connection.cursor() + + # Run SQL DDL. + cursor.execute("CREATE TABLE foobar (id INTEGER PRIMARY KEY, name STRING);") + + # Run a batch insert that is empty. + with self.assertRaises(ProgrammingError) as cm: + cursor.executemany("INSERT INTO foobar (id, name) VALUES (?, ?)", []) + self.assertEqual( + str(cm.exception), + "SQLParseException[The query contains a parameter placeholder $1, " + "but there are only 0 parameter values]") + + cursor.close() + connection.close() + + @unittest.skipIf(sys.version_info < (3, 8), "BulkResponse needs Python 3.8 or higher") + def test_bulk_response_empty_records_or_results(self): + + # Import at runtime is on purpose, to permit skipping the test case. + from crate.client.result import BulkResponse + + with self.assertRaises(ValueError) as cm: + BulkResponse(records=None, results=None) + self.assertEqual( + str(cm.exception), + "Processing a bulk response without records is an invalid operation") + + with self.assertRaises(ValueError) as cm: + BulkResponse(records=[], results=None) + self.assertEqual( + str(cm.exception), + "Processing a bulk response without results is an invalid operation") diff --git a/tests/client/tests.py b/tests/client/tests.py new file mode 100644 index 00000000..423a3206 --- /dev/null +++ b/tests/client/tests.py @@ -0,0 +1,72 @@ +import doctest +import unittest + +from .test_connection import ConnectionTest +from .test_cursor import CursorTest +from .test_http import HttpClientTest, KeepAliveClientTest, ThreadSafeHttpClientTest, ParamsTest, \ + RetryOnTimeoutServerTest, RequestsCaBundleTest, TestUsernameSentAsHeader, TestCrateJsonEncoder, \ + TestDefaultSchemaHeader +from .layer import makeSuite, setUpWithHttps, HttpsTestServerLayer, setUpCrateLayerBaseline, \ + tearDownDropEntitiesBaseline, ensure_cratedb_layer +from .test_result import BulkOperationTest + + +def test_suite(): + suite = unittest.TestSuite() + flags = (doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) + + # Unit tests. + suite.addTest(makeSuite(CursorTest)) + suite.addTest(makeSuite(HttpClientTest)) + suite.addTest(makeSuite(KeepAliveClientTest)) + suite.addTest(makeSuite(ThreadSafeHttpClientTest)) + suite.addTest(makeSuite(ParamsTest)) + suite.addTest(makeSuite(ConnectionTest)) + suite.addTest(makeSuite(RetryOnTimeoutServerTest)) + suite.addTest(makeSuite(RequestsCaBundleTest)) + suite.addTest(makeSuite(TestUsernameSentAsHeader)) + suite.addTest(makeSuite(TestCrateJsonEncoder)) + suite.addTest(makeSuite(TestDefaultSchemaHeader)) + suite.addTest(doctest.DocTestSuite('crate.client.connection')) + suite.addTest(doctest.DocTestSuite('crate.client.http')) + + s = doctest.DocFileSuite( + 'docs/by-example/connection.rst', + 'docs/by-example/cursor.rst', + module_relative=False, + optionflags=flags, + encoding='utf-8' + ) + suite.addTest(s) + + s = doctest.DocFileSuite( + 'docs/by-example/https.rst', + module_relative=False, + setUp=setUpWithHttps, + optionflags=flags, + encoding='utf-8' + ) + s.layer = HttpsTestServerLayer() + suite.addTest(s) + + # Integration tests. + layer = ensure_cratedb_layer() + + s = makeSuite(BulkOperationTest) + s.layer = layer + suite.addTest(s) + + s = doctest.DocFileSuite( + 'docs/by-example/http.rst', + 'docs/by-example/client.rst', + 'docs/by-example/blob.rst', + module_relative=False, + setUp=setUpCrateLayerBaseline, + tearDown=tearDownDropEntitiesBaseline, + optionflags=flags, + encoding='utf-8' + ) + s.layer = layer + suite.addTest(s) + + return suite diff --git a/tests/testing/__init__.py b/tests/testing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/testing/settings.py b/tests/testing/settings.py new file mode 100644 index 00000000..eb99a055 --- /dev/null +++ b/tests/testing/settings.py @@ -0,0 +1,9 @@ +from pathlib import Path + + +def crate_path() -> str: + return str(project_root() / "parts" / "crate") + + +def project_root() -> Path: + return Path(__file__).parent.parent.parent diff --git a/src/crate/testing/test_layer.py b/tests/testing/test_layer.py similarity index 99% rename from src/crate/testing/test_layer.py rename to tests/testing/test_layer.py index aaeca336..38d53922 100644 --- a/src/crate/testing/test_layer.py +++ b/tests/testing/test_layer.py @@ -29,7 +29,7 @@ import urllib3 import crate -from .layer import CrateLayer, prepend_http, http_url_from_host_port, wait_for_http_url +from crate.testing.layer import CrateLayer, prepend_http, http_url_from_host_port, wait_for_http_url from .settings import crate_path diff --git a/src/crate/testing/tests.py b/tests/testing/tests.py similarity index 100% rename from src/crate/testing/tests.py rename to tests/testing/tests.py diff --git a/tox.ini b/tox.ini index 978bd90c..1ea931fa 100644 --- a/tox.ini +++ b/tox.ini @@ -11,4 +11,4 @@ deps = mock urllib3 commands = - zope-testrunner -c --test-path=src + zope-testrunner -c --path=tests diff --git a/versions.cfg b/versions.cfg index 62f7d9f3..6dd217c8 100644 --- a/versions.cfg +++ b/versions.cfg @@ -1,4 +1,4 @@ [versions] -crate_server = 5.1.1 +crate_server = 5.9.2 hexagonit.recipe.download = 1.7.1
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: