From f97af555ef81d96b177e168e0a09cea079f68ce8 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Thu, 31 Oct 2024 10:23:52 +0100 Subject: [PATCH 1/5] Testing: Use CrateDB 5.9.2 for testing --- .github/workflows/tests.yml | 2 +- bootstrap.sh | 2 +- versions.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/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 From 7c11cb572a51796e3d6821dd8a9764ac7f64c58d Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Thu, 31 Oct 2024 10:32:30 +0100 Subject: [PATCH 2/5] Testing: Fix `test_no_connection_exception` ... when another CrateDB is running on the default port 4200. --- src/crate/client/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crate/client/test_http.py b/src/crate/client/test_http.py index 8e547963..76e6ade6 100644 --- a/src/crate/client/test_http.py +++ b/src/crate/client/test_http.py @@ -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() From bbc7b68757a12f146c1ae9931d51229f13034042 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Wed, 2 Oct 2024 22:43:38 +0200 Subject: [PATCH 3/5] Testing: Refactor support code out of `zope.testing` entrypoint `tests.py` is the entrypoint file that will be used by `zope.testing` to discover the test cases on behalf of what's returned from `test_suite`. It is better to not overload it with other support code that may also be needed in other contexts. --- src/crate/client/test_support.py | 273 ++++++++++++++++++++++++++++ src/crate/client/tests.py | 295 ++----------------------------- 2 files changed, 284 insertions(+), 284 deletions(-) create mode 100644 src/crate/client/test_support.py diff --git a/src/crate/client/test_support.py b/src/crate/client/test_support.py new file mode 100644 index 00000000..f9d5b7ff --- /dev/null +++ b/src/crate/client/test_support.py @@ -0,0 +1,273 @@ +# -*- 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. + +from __future__ import absolute_import + +import json +import os +import socket +import unittest +from pprint import pprint +from http.server import HTTPServer, BaseHTTPRequestHandler +import ssl +import time +import threading +import logging + +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 + + +makeSuite = unittest.TestLoader().loadTestsFromTestCase + +log = logging.getLogger('crate.testing.layer') +ch = logging.StreamHandler() +ch.setLevel(logging.ERROR) +log.addHandler(ch) + + +def cprint(s): + if isinstance(s, bytes): + s = s.decode('utf-8') + print(s) + + +settings = { + 'udc.enabled': 'false', + 'lang.js.enabled': 'true', + 'auth.host_based.enabled': 'true', + 'auth.host_based.config.0.user': 'crate', + 'auth.host_based.config.0.method': 'trust', + 'auth.host_based.config.98.user': 'trusted_me', + 'auth.host_based.config.98.method': 'trust', + 'auth.host_based.config.99.user': 'me', + 'auth.host_based.config.99.method': 'password', +} +crate_layer = None + + +def ensure_cratedb_layer(): + """ + In order to skip individual tests by manually disabling them within + `def test_suite()`, it is crucial make the test layer not run on each + and every occasion. So, things like this will be possible:: + + ./bin/test -vvvv --ignore_dir=testing + + TODO: Through a subsequent patch, the possibility to individually + unselect specific tests might be added to `def test_suite()` + on behalf of environment variables. + A blueprint for this kind of logic can be found at + https://github.com/crate/crate/commit/414cd833. + """ + global crate_layer + + if crate_layer is None: + crate_layer = CrateLayer('crate', + crate_home=crate_path(), + port=crate_port, + host=localhost, + transport_port=crate_transport_port, + settings=settings) + return crate_layer + + +def setUpCrateLayerBaseline(test): + 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: + stmt = s.read() + cursor.execute(stmt) + stmt = ("select count(*) from information_schema.tables " + "where table_name = 'locations'") + cursor.execute(stmt) + assert cursor.fetchall()[0][0] == 1 + + data_path = docs_path('testing/testdata/data/test_a.json') + # load testing data into crate + cursor.execute("copy locations from ?", (data_path,)) + # refresh location table so imported data is visible immediately + cursor.execute("refresh table locations") + # create blob table + cursor.execute("create blob table myfiles clustered into 1 shards " + + "with (number_of_replicas=0)") + + # create users + cursor.execute("CREATE USER me WITH (password = 'my_secret_pw')") + cursor.execute("CREATE USER trusted_me") + + cursor.close() + + +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", + "DROP USER trusted_me", + ] + _execute_statements(ddl_statements) + + +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")) + + __name__ = "httpsserver" + __bases__ = tuple() + + class HttpsServer(HTTPServer): + def get_request(self): + + # Prepare SSL context. + context = ssl._create_unverified_context( + protocol=ssl.PROTOCOL_TLS_SERVER, + cert_reqs=ssl.CERT_OPTIONAL, + check_hostname=False, + purpose=ssl.Purpose.CLIENT_AUTH, + certfile=HttpsTestServerLayer.CERT_FILE, + keyfile=HttpsTestServerLayer.CERT_FILE, + cafile=HttpsTestServerLayer.CACERT_FILE) + + # Set minimum protocol version, TLSv1 and TLSv1.1 are unsafe. + context.minimum_version = ssl.TLSVersion.TLSv1_2 + + # Wrap TLS encryption around socket. + socket, client_address = HTTPServer.get_request(self) + socket = context.wrap_socket(socket, server_side=True) + + return socket, client_address + + class HttpsHandler(BaseHTTPRequestHandler): + + payload = json.dumps({"name": "test", "status": 200, }) + + def do_GET(self): + self.send_response(200) + payload = self.payload.encode('UTF-8') + self.send_header("Content-Length", len(payload)) + self.send_header("Content-Type", "application/json; charset=UTF-8") + self.end_headers() + self.wfile.write(payload) + + def setUp(self): + self.server = self.HttpsServer( + (self.HOST, self.PORT), + self.HttpsHandler + ) + thread = threading.Thread(target=self.serve_forever) + thread.daemon = True # quit interpreter when only thread exists + thread.start() + self.waitForServer() + + def serve_forever(self): + print("listening on", self.HOST, self.PORT) + self.server.serve_forever() + print("server stopped.") + + def tearDown(self): + self.server.shutdown() + self.server.server_close() + + def isUp(self): + """ + Test if a host is up. + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ex = s.connect_ex((self.HOST, self.PORT)) + s.close() + return ex == 0 + + def waitForServer(self, timeout=5): + """ + Wait for the host to be available. + """ + with stopit.ThreadingTimeout(timeout) as to_ctx_mgr: + while True: + if self.isUp(): + break + time.sleep(0.001) + + if not to_ctx_mgr: + raise TimeoutError("Could not properly start embedded webserver " + "within {} seconds".format(timeout)) + + +def setUpWithHttps(test): + test.globs['crate_host'] = "https://{0}:{1}".format( + HttpsTestServerLayer.HOST, HttpsTestServerLayer.PORT + ) + 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") + ) + + +def _execute_statements(statements, on_error="ignore"): + with connect(crate_host) as conn: + cursor = conn.cursor() + for stmt in statements: + _execute_statement(cursor, stmt, on_error=on_error) + cursor.close() + + +def _execute_statement(cursor, stmt, on_error="ignore"): + try: + cursor.execute(stmt) + except Exception: # pragma: no cover + # FIXME: Why does this croak on statements like ``DROP TABLE cities``? + # Note: When needing to debug the test environment, you may want to + # enable this logger statement. + # log.exception("Executing SQL statement failed") + if on_error == "ignore": + pass + elif on_error == "raise": + raise diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index 2f6be428..476d37aa 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -1,288 +1,13 @@ -# -*- 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. - -from __future__ import absolute_import - -import json -import os -import socket -import unittest import doctest -from pprint import pprint -from http.server import HTTPServer, BaseHTTPRequestHandler -import ssl -import time -import threading -import logging - -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 .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 - -log = logging.getLogger('crate.testing.layer') -ch = logging.StreamHandler() -ch.setLevel(logging.ERROR) -log.addHandler(ch) - - -def cprint(s): - if isinstance(s, bytes): - s = s.decode('utf-8') - print(s) - - -settings = { - 'udc.enabled': 'false', - 'lang.js.enabled': 'true', - 'auth.host_based.enabled': 'true', - 'auth.host_based.config.0.user': 'crate', - 'auth.host_based.config.0.method': 'trust', - 'auth.host_based.config.98.user': 'trusted_me', - 'auth.host_based.config.98.method': 'trust', - 'auth.host_based.config.99.user': 'me', - 'auth.host_based.config.99.method': 'password', -} -crate_layer = None - - -def ensure_cratedb_layer(): - """ - In order to skip individual tests by manually disabling them within - `def test_suite()`, it is crucial make the test layer not run on each - and every occasion. So, things like this will be possible:: - - ./bin/test -vvvv --ignore_dir=testing - - TODO: Through a subsequent patch, the possibility to individually - unselect specific tests might be added to `def test_suite()` - on behalf of environment variables. - A blueprint for this kind of logic can be found at - https://github.com/crate/crate/commit/414cd833. - """ - global crate_layer - - if crate_layer is None: - crate_layer = CrateLayer('crate', - crate_home=crate_path(), - port=crate_port, - host=localhost, - transport_port=crate_transport_port, - settings=settings) - return crate_layer - - -def setUpCrateLayerBaseline(test): - 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: - stmt = s.read() - cursor.execute(stmt) - stmt = ("select count(*) from information_schema.tables " - "where table_name = 'locations'") - cursor.execute(stmt) - assert cursor.fetchall()[0][0] == 1 - - data_path = docs_path('testing/testdata/data/test_a.json') - # load testing data into crate - cursor.execute("copy locations from ?", (data_path,)) - # refresh location table so imported data is visible immediately - cursor.execute("refresh table locations") - # create blob table - cursor.execute("create blob table myfiles clustered into 1 shards " + - "with (number_of_replicas=0)") - - # create users - cursor.execute("CREATE USER me WITH (password = 'my_secret_pw')") - cursor.execute("CREATE USER trusted_me") - - cursor.close() - - -def tearDownDropEntitiesBaseline(test): - """ - Drop all tables, views, and users created by `setUpWithCrateLayer*`. - """ - ddl_statements = [ - "DROP TABLE locations", - "DROP BLOB TABLE myfiles", - "DROP USER me", - "DROP USER trusted_me", - ] - _execute_statements(ddl_statements) - - -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")) - - __name__ = "httpsserver" - __bases__ = tuple() - - class HttpsServer(HTTPServer): - def get_request(self): - - # Prepare SSL context. - context = ssl._create_unverified_context( - protocol=ssl.PROTOCOL_TLS_SERVER, - cert_reqs=ssl.CERT_OPTIONAL, - check_hostname=False, - purpose=ssl.Purpose.CLIENT_AUTH, - certfile=HttpsTestServerLayer.CERT_FILE, - keyfile=HttpsTestServerLayer.CERT_FILE, - cafile=HttpsTestServerLayer.CACERT_FILE) - - # Set minimum protocol version, TLSv1 and TLSv1.1 are unsafe. - context.minimum_version = ssl.TLSVersion.TLSv1_2 - - # Wrap TLS encryption around socket. - socket, client_address = HTTPServer.get_request(self) - socket = context.wrap_socket(socket, server_side=True) - - return socket, client_address - - class HttpsHandler(BaseHTTPRequestHandler): - - payload = json.dumps({"name": "test", "status": 200, }) - - def do_GET(self): - self.send_response(200) - payload = self.payload.encode('UTF-8') - self.send_header("Content-Length", len(payload)) - self.send_header("Content-Type", "application/json; charset=UTF-8") - self.end_headers() - self.wfile.write(payload) - - def setUp(self): - self.server = self.HttpsServer( - (self.HOST, self.PORT), - self.HttpsHandler - ) - thread = threading.Thread(target=self.serve_forever) - thread.daemon = True # quit interpreter when only thread exists - thread.start() - self.waitForServer() - - def serve_forever(self): - print("listening on", self.HOST, self.PORT) - self.server.serve_forever() - print("server stopped.") - - def tearDown(self): - self.server.shutdown() - self.server.server_close() - - def isUp(self): - """ - Test if a host is up. - """ - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ex = s.connect_ex((self.HOST, self.PORT)) - s.close() - return ex == 0 - - def waitForServer(self, timeout=5): - """ - Wait for the host to be available. - """ - with stopit.ThreadingTimeout(timeout) as to_ctx_mgr: - while True: - if self.isUp(): - break - time.sleep(0.001) - - if not to_ctx_mgr: - raise TimeoutError("Could not properly start embedded webserver " - "within {} seconds".format(timeout)) - - -def setUpWithHttps(test): - test.globs['crate_host'] = "https://{0}:{1}".format( - HttpsTestServerLayer.HOST, HttpsTestServerLayer.PORT - ) - 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") - ) - - -def _execute_statements(statements, on_error="ignore"): - with connect(crate_host) as conn: - cursor = conn.cursor() - for stmt in statements: - _execute_statement(cursor, stmt, on_error=on_error) - cursor.close() - +import unittest -def _execute_statement(cursor, stmt, on_error="ignore"): - try: - cursor.execute(stmt) - except Exception: # pragma: no cover - # FIXME: Why does this croak on statements like ``DROP TABLE cities``? - # Note: When needing to debug the test environment, you may want to - # enable this logger statement. - # log.exception("Executing SQL statement failed") - if on_error == "ignore": - pass - elif on_error == "raise": - raise +from crate.client.test_connection import ConnectionTest +from crate.client.test_cursor import CursorTest +from crate.client.test_http import HttpClientTest, KeepAliveClientTest, ThreadSafeHttpClientTest, ParamsTest, \ + RetryOnTimeoutServerTest, RequestsCaBundleTest, TestUsernameSentAsHeader, TestCrateJsonEncoder, \ + TestDefaultSchemaHeader +from crate.client.test_support import makeSuite, setUpWithHttps, HttpsTestServerLayer, setUpCrateLayerBaseline, \ + tearDownDropEntitiesBaseline, ensure_cratedb_layer def test_suite(): @@ -324,6 +49,8 @@ def test_suite(): suite.addTest(s) # Integration tests. + layer = ensure_cratedb_layer() + s = doctest.DocFileSuite( 'docs/by-example/http.rst', 'docs/by-example/client.rst', @@ -334,7 +61,7 @@ def test_suite(): optionflags=flags, encoding='utf-8' ) - s.layer = ensure_cratedb_layer() + s.layer = layer suite.addTest(s) return suite From 2b6b835da7aea6bc4c99eafe3548479124b89e22 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Thu, 31 Oct 2024 14:09:33 +0100 Subject: [PATCH 4/5] Testing: Refactor software tests into dedicated directory `tests` git mv src/crate/client/test* tests/client/ git mv src/crate/testing/test* tests/testing/ --- CHANGES.txt | 2 + DEVELOP.rst | 24 ++++--- bin/test | 6 +- docs/by-example/connection.rst | 2 +- docs/by-example/cursor.rst | 2 +- src/crate/client/test_util.py | 69 ------------------ src/crate/testing/util.py | 71 +++++++++++++++++++ tests/__init__.py | 0 .../data => tests/assets/import}/test_a.json | 0 .../assets}/mappings/locations.sql | 0 .../assets}/pki/cacert_invalid.pem | 0 .../assets}/pki/cacert_valid.pem | 0 .../assets}/pki/client_invalid.pem | 0 .../assets}/pki/client_valid.pem | 0 .../client => tests/assets}/pki/readme.rst | 0 .../assets}/pki/server_valid.pem | 0 .../assets}/settings/test_a.json | 0 tests/client/__init__.py | 0 .../test_support.py => tests/client/layer.py | 34 ++++----- .../testing => tests/client}/settings.py | 23 +++--- .../crate => tests}/client/test_connection.py | 6 +- {src/crate => tests}/client/test_cursor.py | 2 +- .../crate => tests}/client/test_exceptions.py | 0 {src/crate => tests}/client/test_http.py | 4 +- {src/crate => tests}/client/tests.py | 8 +-- tests/testing/__init__.py | 0 tests/testing/settings.py | 9 +++ {src/crate => tests}/testing/test_layer.py | 2 +- {src/crate => tests}/testing/tests.py | 0 tox.ini | 2 +- 30 files changed, 134 insertions(+), 132 deletions(-) delete mode 100644 src/crate/client/test_util.py create mode 100644 tests/__init__.py rename {src/crate/testing/testdata/data => tests/assets/import}/test_a.json (100%) rename {src/crate/testing/testdata => tests/assets}/mappings/locations.sql (100%) rename {src/crate/client => tests/assets}/pki/cacert_invalid.pem (100%) rename {src/crate/client => tests/assets}/pki/cacert_valid.pem (100%) rename {src/crate/client => tests/assets}/pki/client_invalid.pem (100%) rename {src/crate/client => tests/assets}/pki/client_valid.pem (100%) rename {src/crate/client => tests/assets}/pki/readme.rst (100%) rename {src/crate/client => tests/assets}/pki/server_valid.pem (100%) rename {src/crate/testing/testdata => tests/assets}/settings/test_a.json (100%) create mode 100644 tests/client/__init__.py rename src/crate/client/test_support.py => tests/client/layer.py (88%) rename {src/crate/testing => tests/client}/settings.py (77%) rename {src/crate => tests}/client/test_connection.py (96%) rename {src/crate => tests}/client/test_cursor.py (99%) rename {src/crate => tests}/client/test_exceptions.py (100%) rename {src/crate => tests}/client/test_http.py (99%) rename {src/crate => tests}/client/tests.py (85%) create mode 100644 tests/testing/__init__.py create mode 100644 tests/testing/settings.py rename {src/crate => tests}/testing/test_layer.py (99%) rename {src/crate => tests}/testing/tests.py (100%) diff --git a/CHANGES.txt b/CHANGES.txt index 4a0f0a48..4c71ea4a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,8 @@ 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``. .. _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/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/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/test_support.py b/tests/client/layer.py similarity index 88% rename from src/crate/client/test_support.py rename to tests/client/layer.py index f9d5b7ff..b2d521e7 100644 --- a/src/crate/client/test_support.py +++ b/tests/client/layer.py @@ -34,11 +34,11 @@ 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 makeSuite = unittest.TestLoader().loadTestsFromTestCase @@ -104,7 +104,7 @@ def setUpCrateLayerBaseline(test): 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 " @@ -112,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 @@ -145,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() @@ -237,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"): 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 99% rename from src/crate/client/test_http.py rename to tests/client/test_http.py index 76e6ade6..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() diff --git a/src/crate/client/tests.py b/tests/client/tests.py similarity index 85% rename from src/crate/client/tests.py rename to tests/client/tests.py index 476d37aa..10c2f03d 100644 --- a/src/crate/client/tests.py +++ b/tests/client/tests.py @@ -1,12 +1,12 @@ import doctest import unittest -from crate.client.test_connection import ConnectionTest -from crate.client.test_cursor import CursorTest -from crate.client.test_http import HttpClientTest, KeepAliveClientTest, ThreadSafeHttpClientTest, ParamsTest, \ +from .test_connection import ConnectionTest +from .test_cursor import CursorTest +from .test_http import HttpClientTest, KeepAliveClientTest, ThreadSafeHttpClientTest, ParamsTest, \ RetryOnTimeoutServerTest, RequestsCaBundleTest, TestUsernameSentAsHeader, TestCrateJsonEncoder, \ TestDefaultSchemaHeader -from crate.client.test_support import makeSuite, setUpWithHttps, HttpsTestServerLayer, setUpCrateLayerBaseline, \ +from .layer import makeSuite, setUpWithHttps, HttpsTestServerLayer, setUpCrateLayerBaseline, \ tearDownDropEntitiesBaseline, ensure_cratedb_layer 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 From a0161301065e79c320492c0a2566cf87c806b9e3 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Wed, 2 Oct 2024 22:43:38 +0200 Subject: [PATCH 5/5] BulkResponse: Add wrapper for improved decoding of HTTP bulk responses CrateDB HTTP bulk responses include `rowcount=` items, either signalling if a bulk operation succeeded or failed. - success means `rowcount=1` - failure means `rowcount=-2` https://cratedb.com/docs/crate/reference/en/latest/interfaces/http.html#error-handling --- CHANGES.txt | 2 + src/crate/client/result.py | 68 ++++++++++++++++++++++++++++ tests/client/test_result.py | 88 +++++++++++++++++++++++++++++++++++++ tests/client/tests.py | 5 +++ 4 files changed, 163 insertions(+) create mode 100644 src/crate/client/result.py create mode 100644 tests/client/test_result.py diff --git a/CHANGES.txt b/CHANGES.txt index 4c71ea4a..aac5886b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,6 +14,8 @@ Unreleased 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/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/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 index 10c2f03d..423a3206 100644 --- a/tests/client/tests.py +++ b/tests/client/tests.py @@ -8,6 +8,7 @@ TestDefaultSchemaHeader from .layer import makeSuite, setUpWithHttps, HttpsTestServerLayer, setUpCrateLayerBaseline, \ tearDownDropEntitiesBaseline, ensure_cratedb_layer +from .test_result import BulkOperationTest def test_suite(): @@ -51,6 +52,10 @@ def test_suite(): # 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', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy