diff --git a/testkitbackend/backend.py b/testkitbackend/backend.py index beed90b7..24990c42 100644 --- a/testkitbackend/backend.py +++ b/testkitbackend/backend.py @@ -165,6 +165,8 @@ def _process(self, request): if isinstance(e, Neo4jError): payload["code"] = e.code self.send_response("DriverError", payload) + except requests.FrontendError as e: + self.send_response("FrontendError", {"msg": str(e)}) except Exception: tb = traceback.format_exc() log.error(tb) diff --git a/testkitbackend/fromtestkit.py b/testkitbackend/fromtestkit.py index 40bad08a..2596a6f9 100644 --- a/testkitbackend/fromtestkit.py +++ b/testkitbackend/fromtestkit.py @@ -36,7 +36,7 @@ def to_meta_and_timeout(data): metadata.mark_all_as_read() timeout = data.get('timeout', None) if timeout: - timeout = float(timeout) / 1000 + timeout = timeout / 1000 return metadata, timeout diff --git a/testkitbackend/requests.py b/testkitbackend/requests.py index 10f68f6a..53f595f9 100644 --- a/testkitbackend/requests.py +++ b/testkitbackend/requests.py @@ -23,6 +23,10 @@ from testkitbackend.fromtestkit import to_meta_and_timeout +class FrontendError(Exception): + pass + + def load_config(): with open(path.join(path.dirname(__file__), "test_config.json"), "r") as fd: config = json.load(fd) @@ -193,7 +197,7 @@ def SessionRun(backend, data): result = session.run(query, parameters=params) key = backend.next_key() backend.results[key] = result - backend.send_response("Result", {"id": key}) + backend.send_response("Result", {"id": key, "keys": result.keys()}) def SessionClose(backend, data): @@ -244,7 +248,7 @@ def func(tx): if session_tracker.error_id: raise backend.errors[session_tracker.error_id] else: - raise Exception("Client said no") + raise FrontendError("Client said no") if is_read: session.read_transaction(func) @@ -270,7 +274,7 @@ def TransactionRun(backend, data): result = tx.run(cypher, parameters=params) key = backend.next_key() backend.results[key] = result - backend.send_response("Result", {"id": key}) + backend.send_response("Result", {"id": key, "keys": result.keys()}) def TransactionCommit(backend, data): @@ -300,13 +304,43 @@ def ResultNext(backend, data): def ResultConsume(backend, data): result = backend.results[data["resultId"]] summary = result.consume() + from neo4j.work.summary import ResultSummary + assert isinstance(summary, ResultSummary) backend.send_response("Summary", { "serverInfo": { + "address": ":".join(map(str, summary.server.address)), + "agent": summary.server.agent, "protocolVersion": ".".join(map(str, summary.server.protocol_version)), - "agent": summary.server.agent, - "address": ":".join(map(str, summary.server.address)), - } + }, + "counters": None if not summary.counters else { + "constraintsAdded": summary.counters.constraints_added, + "constraintsRemoved": summary.counters.constraints_removed, + "containsSystemUpdates": summary.counters.contains_system_updates, + "containsUpdates": summary.counters.contains_updates, + "indexesAdded": summary.counters.indexes_added, + "indexesRemoved": summary.counters.indexes_removed, + "labelsAdded": summary.counters.labels_added, + "labelsRemoved": summary.counters.labels_removed, + "nodesCreated": summary.counters.nodes_created, + "nodesDeleted": summary.counters.nodes_deleted, + "propertiesSet": summary.counters.properties_set, + "relationshipsCreated": summary.counters.relationships_created, + "relationshipsDeleted": summary.counters.relationships_deleted, + "systemUpdates": summary.counters.system_updates, + }, + "database": summary.database, + "notifications": summary.notifications, + "plan": summary.plan, + "profile": summary.profile, + "query": { + "text": summary.query, + "parameters": {k: totestkit.field(v) + for k, v in summary.parameters.items()}, + }, + "queryType": summary.query_type, + "resultAvailableAfter": summary.result_available_after, + "resultConsumedAfter": summary.result_consumed_after, }) diff --git a/testkitbackend/test_config.json b/testkitbackend/test_config.json index 5f6703fd..6eb9e2f3 100644 --- a/testkitbackend/test_config.json +++ b/testkitbackend/test_config.json @@ -32,6 +32,9 @@ "Optimization:ImplicitDefaultArguments": true, "Optimization:MinimalResets": "Driver resets some clean connections when put back into pool", "Optimization:ConnectionReuse": true, - "Optimization:PullPipelining": true + "Optimization:PullPipelining": true, + "Temporary:ResultKeys": true, + "Temporary:FullSummary": true, + "Temporary:CypherPathAndRelationship": true } } diff --git a/testkitbackend/totestkit.py b/testkitbackend/totestkit.py index 960f4530..44bd7ab6 100644 --- a/testkitbackend/totestkit.py +++ b/testkitbackend/totestkit.py @@ -14,7 +14,11 @@ # 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. -from neo4j.graph import Node +from neo4j.graph import ( + Node, + Path, + Relationship, +) def record(rec): @@ -55,5 +59,20 @@ def to(name, val): "props": field(v._properties), } return {"name": "Node", "data": node} + if isinstance(v, Relationship): + rel = { + "id": field(v.id), + "startNodeId": field(v.start_node.id), + "endNodeId": field(v.end_node.id), + "type": field(v.type), + "props": field(v._properties), + } + return {"name": "Relationship", "data": rel} + if isinstance(v, Path): + path = { + "nodes": field(list(v.nodes)), + "relationships": field(list(v.relationships)), + } + return {"name": "Path", "data": path} raise Exception("Unhandled type:" + str(type(v))) diff --git a/tests/integration/test_autocommit.py b/tests/integration/test_autocommit.py index deabb54e..32619991 100644 --- a/tests/integration/test_autocommit.py +++ b/tests/integration/test_autocommit.py @@ -19,228 +19,11 @@ # limitations under the License. -import pytest - from neo4j.work.simple import Query -from neo4j.exceptions import Neo4jError, ClientError, TransientError -from neo4j.graph import Node, Relationship -from neo4j.api import Version - - -def test_can_run_simple_statement(session): - result = session.run("RETURN 1 AS n") - for record in result: - assert record[0] == 1 - assert record["n"] == 1 - with pytest.raises(KeyError): - _ = record["x"] - assert record["n"] == 1 - with pytest.raises(KeyError): - _ = record["x"] - with pytest.raises(TypeError): - _ = record[object()] - assert repr(record) - assert len(record) == 1 - - -def test_can_run_simple_statement_with_params(session): - count = 0 - for record in session.run("RETURN $x AS n", - {"x": {"abc": ["d", "e", "f"]}}): - assert record[0] == {"abc": ["d", "e", "f"]} - assert record["n"] == {"abc": ["d", "e", "f"]} - assert repr(record) - assert len(record) == 1 - count += 1 - assert count == 1 - - -def test_autocommit_transactions_use_bookmarks(neo4j_driver): - bookmarks = [] - # Generate an initial bookmark - with neo4j_driver.session() as session: - session.run("CREATE ()").consume() - bookmark = session.last_bookmark() - assert bookmark is not None - bookmarks.append(bookmark) - # Propagate into another session - with neo4j_driver.session(bookmarks=bookmarks) as session: - assert list(session._bookmarks) == bookmarks - session.run("CREATE ()").consume() - bookmark = session.last_bookmark() - assert bookmark is not None - assert bookmark not in bookmarks - - -def test_fails_on_bad_syntax(session): - with pytest.raises(Neo4jError): - session.run("X").consume() - - -def test_fails_on_missing_parameter(session): - with pytest.raises(Neo4jError): - session.run("RETURN {x}").consume() - - -def test_keys_with_an_error(session): - with pytest.raises(Neo4jError): - result = session.run("X") - list(result.keys()) - - -def test_should_not_allow_empty_statements(session): - with pytest.raises(ValueError): - _ = session.run("") - - -def test_can_run_statement_that_returns_multiple_records(session): - count = 0 - for record in session.run("unwind(range(1, 10)) AS z RETURN z"): - assert 1 <= record[0] <= 10 - count += 1 - assert count == 10 - - -def test_can_use_with_to_auto_close_session(session): - record_list = list(session.run("RETURN 1")) - assert len(record_list) == 1 - for record in record_list: - assert record[0] == 1 - - -def test_can_return_node(neo4j_driver): - with neo4j_driver.session() as session: - record_list = list(session.run("CREATE (a:Person {name:'Alice'}) " - "RETURN a")) - assert len(record_list) == 1 - for record in record_list: - alice = record[0] - assert isinstance(alice, Node) - assert alice.labels == {"Person"} - assert dict(alice) == {"name": "Alice"} - - -def test_can_return_relationship(neo4j_driver): - with neo4j_driver.session() as session: - record_list = list(session.run("CREATE ()-[r:KNOWS {since:1999}]->() " - "RETURN r")) - assert len(record_list) == 1 - for record in record_list: - rel = record[0] - assert isinstance(rel, Relationship) - assert rel.type == "KNOWS" - assert dict(rel) == {"since": 1999} - - -# TODO: re-enable after server bug is fixed -# def test_can_return_path(session): -# with self.driver.session() as session: -# record_list = list(session.run("MERGE p=({name:'Alice'})-[:KNOWS]->" -# "({name:'Bob'}) RETURN p")) -# assert len(record_list) == 1 -# for record in record_list: -# path = record[0] -# assert isinstance(path, Path) -# assert path.start_node["name"] == "Alice" -# assert path.end_node["name"] == "Bob" -# assert path.relationships[0].type == "KNOWS" -# assert len(path.nodes) == 2 -# assert len(path.relationships) == 1 - - -def test_keys_are_available_before_and_after_stream(session): - result = session.run("UNWIND range(1, 10) AS n RETURN n") - assert list(result.keys()) == ["n"] - list(result) - assert list(result.keys()) == ["n"] +# TODO: this test will stay until a uniform behavior for `.single()` across the +# drivers has been specified and tests are created in testkit def test_result_single_record_value(session): record = session.run(Query("RETURN $x"), x=1).single() assert record.value() == 1 - - -@pytest.mark.parametrize( - "test_input, neo4j_version", - [ - ("CALL dbms.getTXMetaData", Version(3, 0)), - ("CALL tx.getMetaData", Version(4, 0)), - ] -) -def test_autocommit_transactions_should_support_metadata(session, test_input, neo4j_version): - # python -m pytest tests/integration/test_autocommit.py -s -r fEsxX -k test_autocommit_transactions_should_support_metadata - metadata_in = {"foo": "bar"} - - result = session.run("RETURN 1") - value = result.single().value() - summary = result.consume() - server_agent = summary.server.agent - - try: - statement = Query(test_input, metadata=metadata_in) - result = session.run(statement) - metadata_out = result.single().value() - except ClientError as e: - if e.code == "Neo.ClientError.Procedure.ProcedureNotFound": - pytest.skip("Cannot assert correct metadata as {} does not support procedure '{}' introduced in Neo4j {}".format(server_agent, test_input, neo4j_version)) - else: - raise - else: - assert metadata_in == metadata_out - - -def test_autocommit_transactions_should_support_timeout(neo4j_driver): - with neo4j_driver.session() as s1: - s1.run("CREATE (a:Node)").consume() - with neo4j_driver.session() as s2: - tx1 = s1.begin_transaction() - tx1.run("MATCH (a:Node) SET a.property = 1").consume() - try: - result = s2.run(Query("MATCH (a:Node) SET a.property = 2", timeout=0.25)) - result.consume() - # On 4.0 and older - except TransientError: - pass - # On 4.1 and forward - except ClientError: - pass - else: - raise - - -def test_regex_in_parameter(session): - matches = [] - result = session.run("UNWIND ['A', 'B', 'C', 'A B', 'B C', 'A B C', " - "'A BC', 'AB C'] AS t WITH t " - "WHERE t =~ $re RETURN t", re=r'.*\bB\b.*') - for record in result: - matches.append(record.value()) - assert matches == ["B", "A B", "B C", "A B C"] - - -def test_regex_inline(session): - matches = [] - result = session.run(r"UNWIND ['A', 'B', 'C', 'A B', 'B C', 'A B C', " - r"'A BC', 'AB C'] AS t WITH t " - r"WHERE t =~ '.*\\bB\\b.*' RETURN t") - for record in result: - matches.append(record.value()) - assert matches == ["B", "A B", "B C", "A B C"] - - -def test_automatic_reset_after_failure(session): - try: - result = session.run("X") - result.consume() - except Neo4jError: - result = session.run("RETURN 1") - record = next(iter(result)) - assert record[0] == 1 - else: - assert False, "A Cypher error should have occurred" - - -def test_large_values(bolt_driver): - for i in range(1, 7): - with bolt_driver.session() as session: - session.run("RETURN '{}'".format("A" * 2 ** 20)) diff --git a/tests/integration/test_bolt_driver.py b/tests/integration/test_bolt_driver.py index 238213cf..2326191b 100644 --- a/tests/integration/test_bolt_driver.py +++ b/tests/integration/test_bolt_driver.py @@ -43,28 +43,9 @@ from neo4j._exceptions import BoltHandshakeError from neo4j.io._bolt3 import Bolt3 -# python -m pytest tests/integration/test_bolt_driver.py -s -v - - -def test_bolt_uri(bolt_uri, auth): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_uri - try: - with GraphDatabase.driver(bolt_uri, auth=auth) as driver: - with driver.session() as session: - value = session.run("RETURN 1").single().value() - assert value == 1 - except ServiceUnavailable as error: - assert isinstance(error.__cause__, BoltHandshakeError) - pytest.skip(error.args[0]) - - -# def test_readonly_bolt_uri(readonly_bolt_uri, auth): -# with GraphDatabase.driver(readonly_bolt_uri, auth=auth) as driver: -# with driver.session() as session: -# value = session.run("RETURN 1").single().value() -# assert value == 1 - +# TODO: this test will stay until a uniform behavior for `.single()` across the +# drivers has been specified and tests are created in testkit def test_normal_use_case(bolt_driver): # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_normal_use_case session = bolt_driver.session() @@ -72,121 +53,13 @@ def test_normal_use_case(bolt_driver): assert value == 1 -def test_invalid_url_scheme(service): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_invalid_url_scheme - address = service.addresses[0] - uri = "x://{}:{}".format(address[0], address[1]) - try: - with pytest.raises(ConfigurationError): - _ = GraphDatabase.driver(uri, auth=service.auth) - except ServiceUnavailable as error: - if isinstance(error.__cause__, BoltHandshakeError): - pytest.skip(error.args[0]) - - -def test_fail_nicely_when_using_http_port(service): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_fail_nicely_when_using_http_port - from tests.integration.conftest import NEO4J_PORTS - address = service.addresses[0] - uri = "bolt://{}:{}".format(address[0], NEO4J_PORTS["http"]) - with pytest.raises(ServiceUnavailable): - driver = GraphDatabase.driver(uri, auth=service.auth) - driver.verify_connectivity() - - -def test_custom_resolver(service): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_custom_resolver - _, port = service.addresses[0] - - def my_resolver(socket_address): - assert socket_address == ("*", 7687) - yield "99.99.99.99", port # should be rejected as unable to connect - yield "127.0.0.1", port # should succeed - - try: - with GraphDatabase.driver("bolt://*", auth=service.auth, - connection_timeout=3, # enables rapid timeout - resolver=my_resolver) as driver: - with driver.session() as session: - summary = session.run("RETURN 1").consume() - assert summary.server.address == ("127.0.0.1", port) - except ServiceUnavailable as error: - if isinstance(error.__cause__, BoltHandshakeError): - pytest.skip(error.args[0]) - - +# TODO: this test will stay until a uniform behavior for `.encrypted` across the +# drivers has been specified and tests are created in testkit def test_encrypted_set_to_false_by_default(bolt_driver): # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_encrypted_set_to_false_by_default assert bolt_driver.encrypted is False -def test_should_fail_on_incorrect_password(bolt_uri): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_should_fail_on_incorrect_password - with pytest.raises(AuthError): - try: - with GraphDatabase.driver(bolt_uri, auth=("neo4j", "wrong-password")) as driver: - with driver.session() as session: - _ = session.run("RETURN 1") - except ServiceUnavailable as error: - if isinstance(error.__cause__, BoltHandshakeError): - pytest.skip(error.args[0]) - - -def test_supports_multi_db(bolt_uri, auth): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_supports_multi_db - try: - driver = GraphDatabase.driver(bolt_uri, auth=auth) - assert isinstance(driver, BoltDriver) - except ServiceUnavailable as error: - if isinstance(error.__cause__, BoltHandshakeError): - pytest.skip(error.args[0]) - - with driver.session() as session: - result = session.run("RETURN 1") - _ = result.single().value() # Consumes the result - summary = result.consume() - server_info = summary.server - - assert isinstance(summary, ResultSummary) - assert isinstance(server_info, ServerInfo) - assert server_info.version_info() is not None - assert isinstance(server_info.protocol_version, Version) - - result = driver.supports_multi_db() - driver.close() - - if server_info.protocol_version == Bolt3.PROTOCOL_VERSION: - assert result is False - assert summary.database is None - assert summary.query_type == "r" - else: - assert result is True - assert server_info.version_info() >= Version(4, 0) - assert server_info.protocol_version >= Version(4, 0) - assert summary.database == "neo4j" # This is the default database name if not set explicitly on the Neo4j Server - assert summary.query_type == "r" - - -def test_test_multi_db_specify_database(bolt_uri, auth): - # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_test_multi_db_specify_database - try: - with GraphDatabase.driver(bolt_uri, auth=auth, database="test_database") as driver: - with driver.session() as session: - result = session.run("RETURN 1") - result_iter = iter(result) - assert next(result_iter) == 1 - summary = result.consume() - assert summary.database == "test_database" - except ServiceUnavailable as error: - if isinstance(error.__cause__, BoltHandshakeError): - pytest.skip(error.args[0]) - except ConfigurationError as error: - assert "Database name parameter for selecting database is not supported in Bolt Protocol Version(3, 0)" in error.args[0] - except ClientError as error: - # FAILURE {'code': 'Neo.ClientError.Database.DatabaseNotFound' - This message is sent from the server - assert error.args[0] == "Database does not exist. Database name: 'test_database'." - - def test_bolt_driver_fetch_size_config_case_on_close_result_consume(bolt_uri, auth): # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_fetch_size_config_case_on_close_result_consume try: diff --git a/tests/integration/test_summary.py b/tests/integration/test_summary.py deleted file mode 100644 index 092b0bd3..00000000 --- a/tests/integration/test_summary.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf-8 -*- - -# Copyright (c) "Neo4j" -# Neo4j Sweden AB [http://neo4j.com] -# -# This file is part of Neo4j. -# -# Licensed 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. - -import pytest - -from neo4j import ( - ResultSummary, - SummaryCounters, - GraphDatabase, -) -from neo4j.exceptions import ( - ServiceUnavailable, -) -from neo4j._exceptions import ( - BoltHandshakeError, -) - - -def test_can_obtain_summary_after_consuming_result(session): - # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_summary_after_consuming_result - - result = session.run("CREATE (n) RETURN n") - summary = result.consume() - assert summary.query == "CREATE (n) RETURN n" - assert summary.parameters == {} - assert summary.query_type == "rw" - assert summary.counters.nodes_created == 1 - - -def test_no_plan_info(session): - result = session.run("CREATE (n) RETURN n") - summary = result.consume() - assert summary.plan is None - assert summary.profile is None - - -def test_can_obtain_plan_info(session): - # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_plan_info - result = session.run("EXPLAIN CREATE (n) RETURN n") - summary = result.consume() - assert isinstance(summary.plan, dict) - - -def test_can_obtain_profile_info(session): - # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_profile_info - result = session.run("PROFILE CREATE (n) RETURN n") - summary = result.consume() - assert isinstance(summary.profile, dict) - - -def test_no_notification_info(session): - # python -m pytest tests/integration/test_summary.py -s -v -k test_no_notification_info - result = session.run("CREATE (n) RETURN n") - summary = result.consume() - assert summary.notifications is None - - -def test_can_obtain_notification_info(session): - # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_notification_info - result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m") - summary = result.consume() - assert isinstance(summary, ResultSummary) - - notifications = summary.notifications - assert isinstance(notifications, list) - assert len(notifications) == 1 - assert isinstance(notifications[0], dict) - - -def test_contains_time_information(session): - summary = session.run("UNWIND range(1,1000) AS n RETURN n AS number").consume() - - assert isinstance(summary.result_available_after, int) - assert isinstance(summary.result_consumed_after, int) - - with pytest.raises(AttributeError) as ex: - assert isinstance(summary.t_first, int) - - with pytest.raises(AttributeError) as ex: - assert isinstance(summary.t_last, int) - - -def test_protocol_version_information(session): - summary = session.run("UNWIND range(1,100) AS n RETURN n AS number").consume() - - assert isinstance(summary.server.protocol_version, tuple) - assert isinstance(summary.server.protocol_version[0], int) - assert isinstance(summary.server.protocol_version[1], int) - - -def test_summary_counters_case_1(session): - # python -m pytest tests/integration/test_summary.py -s -v -k test_summary_counters_case_1 - - result = session.run("RETURN $number AS x", number=3) - summary = result.consume() - - assert summary.query == "RETURN $number AS x" - assert summary.parameters == {"number": 3} - - assert isinstance(summary.query_type, str) - - counters = summary.counters - - assert isinstance(counters, SummaryCounters) - assert counters.nodes_created == 0 - assert counters.nodes_deleted == 0 - assert counters.relationships_created == 0 - assert counters.relationships_deleted == 0 - assert counters.properties_set == 0 - assert counters.labels_added == 0 - assert counters.labels_removed == 0 - assert counters.indexes_added == 0 - assert counters.indexes_removed == 0 - assert counters.constraints_added == 0 - assert counters.constraints_removed == 0 - assert counters.contains_updates is False - - assert counters.system_updates == 0 - assert counters.contains_system_updates is False - - -def test_summary_counters_case_2(neo4j_uri, auth, target, requires_bolt_4x): - # python -m pytest tests/integration/test_summary.py -s -v -k test_summary_counters_case_2 - with GraphDatabase.driver(neo4j_uri, auth=auth) as driver: - - with driver.session(database="system") as session: - session.run("DROP DATABASE test IF EXISTS").consume() - - # SHOW DATABASES - - result = session.run("SHOW DATABASES") - databases = set() - for record in result: - databases.add(record.get("name")) - assert "system" in databases - assert "neo4j" in databases - - summary = result.consume() - - assert summary.query == "SHOW DATABASES" - assert summary.parameters == {} - - assert isinstance(summary.query_type, str) - - counters = summary.counters - - assert isinstance(counters, SummaryCounters) - assert counters.nodes_created == 0 - assert counters.nodes_deleted == 0 - assert counters.relationships_created == 0 - assert counters.relationships_deleted == 0 - assert counters.properties_set == 0 - assert counters.labels_added == 0 - assert counters.labels_removed == 0 - assert counters.indexes_added == 0 - assert counters.indexes_removed == 0 - assert counters.constraints_added == 0 - assert counters.constraints_removed == 0 - assert counters.contains_updates is False - - assert counters.system_updates == 0 - assert counters.contains_system_updates is False - - # CREATE DATABASE test - - summary = session.run("CREATE DATABASE test").consume() - - assert summary.query == "CREATE DATABASE test" - assert summary.parameters == {} - - assert isinstance(summary.query_type, str) - - counters = summary.counters - - assert isinstance(counters, SummaryCounters) - assert counters.nodes_created == 0 - assert counters.nodes_deleted == 0 - assert counters.relationships_created == 0 - assert counters.relationships_deleted == 0 - assert counters.properties_set == 0 - assert counters.labels_added == 0 - assert counters.labels_removed == 0 - assert counters.indexes_added == 0 - assert counters.indexes_removed == 0 - assert counters.constraints_added == 0 - assert counters.constraints_removed == 0 - assert counters.contains_updates is False - - assert counters.system_updates == 1 - assert counters.contains_system_updates is True - - with driver.session(database="test") as session: - summary = session.run("CREATE (n)").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.nodes_created == 1 - - with driver.session(database="test") as session: - summary = session.run("MATCH (n) DELETE (n)").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.nodes_deleted == 1 - - with driver.session(database="test") as session: - summary = session.run("CREATE ()-[:KNOWS]->()").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.relationships_created == 1 - - with driver.session(database="test") as session: - summary = session.run("MATCH ()-[r:KNOWS]->() DELETE r").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.relationships_deleted == 1 - - with driver.session(database="test") as session: - summary = session.run("CREATE (n:ALabel)").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.labels_added == 1 - - with driver.session(database="test") as session: - summary = session.run("MATCH (n:ALabel) REMOVE n:ALabel").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.labels_removed == 1 - - with driver.session(database="test") as session: - summary = session.run("CREATE (n {magic: 42})").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.properties_set == 1 - - with driver.session(database="test") as session: - summary = session.run("CREATE INDEX ON :ALabel(prop)").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.indexes_added == 1 - - with driver.session(database="test") as session: - summary = session.run("DROP INDEX ON :ALabel(prop)").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.indexes_removed == 1 - - with driver.session(database="test") as session: - summary = session.run("CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.constraints_added == 1 - - with driver.session(database="test") as session: - summary = session.run("DROP CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE").consume() - assert summary.counters.contains_updates is True - assert summary.counters.contains_system_updates is False - assert summary.counters.constraints_removed == 1 - - with driver.session(database="system") as session: - session.run("DROP DATABASE test IF EXISTS").consume() diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 1431914b..3e06bf8b 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -328,6 +328,7 @@ def test_serverinfo_initialization(): [ ({"server": "Neo4j/3.0.0"}, "Neo4j/3.0.0", (3, 0, 0)), ({"server": "Neo4j/3.X.Y"}, "Neo4j/3.X.Y", (3, "X", "Y")), + ({"server": "Neo4j/4.3.1"}, "Neo4j/4.3.1", (4, 3, 1)), ] ) def test_serverinfo_with_metadata(test_input, expected_agent, expected_version_info): @@ -342,7 +343,8 @@ def test_serverinfo_with_metadata(test_input, expected_agent, expected_version_i server_info.update(test_input) assert server_info.agent == expected_agent - assert server_info.version_info() == expected_version_info + with pytest.warns(DeprecationWarning): + assert server_info.version_info() == expected_version_info @pytest.mark.parametrize( diff --git a/tests/unit/test_driver.py b/tests/unit/test_driver.py index 588fb9df..1e35cbf4 100644 --- a/tests/unit/test_driver.py +++ b/tests/unit/test_driver.py @@ -56,8 +56,10 @@ def test_routing_driver_constructor(protocol, host, port, auth_token): @pytest.mark.parametrize("test_uri", ( "bolt+ssc://127.0.0.1:9001", "bolt+s://127.0.0.1:9001", + "bolt://127.0.0.1:9001", "neo4j+ssc://127.0.0.1:9001", "neo4j+s://127.0.0.1:9001", + "neo4j://127.0.0.1:9001", )) @pytest.mark.parametrize( ("test_config", "expected_failure", "expected_failure_message"), @@ -81,10 +83,25 @@ def test_routing_driver_constructor(protocol, host, port, auth_token): def test_driver_config_error( test_uri, test_config, expected_failure, expected_failure_message ): - with pytest.raises(expected_failure, match=expected_failure_message): + if "+" in test_uri: + # `+s` and `+ssc` are short hand syntax for not having to configure the + # encryption behavior of the driver. Specifying both is invalid. + with pytest.raises(expected_failure, match=expected_failure_message): + GraphDatabase.driver(test_uri, **test_config) + else: GraphDatabase.driver(test_uri, **test_config) +@pytest.mark.parametrize("test_uri", ( + "http://localhost:9001", + "ftp://localhost:9001", + "x://localhost:9001", +)) +def test_invalid_protocol(test_uri): + with pytest.raises(ConfigurationError, match="scheme"): + GraphDatabase.driver(test_uri) + + @pytest.mark.parametrize( ("test_config", "expected_failure", "expected_failure_message"), ( diff --git a/tests/unit/test_record.py b/tests/unit/test_record.py index 1f13e6e6..37356c54 100644 --- a/tests/unit/test_record.py +++ b/tests/unit/test_record.py @@ -180,3 +180,25 @@ def test_record_get_by_name(): def test_record_get_by_out_of_bounds_index(): r = Record(zip(["name", "age", "married"], ["Alice", 33, True])) assert r[9] is None + + +def test_record_get_item(): + r = Record(zip(["x", "y"], ["foo", "bar"])) + assert r["x"] == "foo" + assert r["y"] == "bar" + with pytest.raises(KeyError): + _ = r["z"] + with pytest.raises(TypeError): + _ = r[object()] + + +@pytest.mark.parametrize("len_", (0, 1, 2, 42)) +def test_record_len(len_): + r = Record(("key_%i" % i, "val_%i" % i) for i in range(len_)) + assert len(r) == len_ + + +@pytest.mark.parametrize("len_", range(3)) +def test_record_repr(len_): + r = Record(("key_%i" % i, "val_%i" % i) for i in range(len_)) + assert repr(r) diff --git a/tests/unit/work/test_result.py b/tests/unit/work/test_result.py index 3136f50f..aaa5dfb8 100644 --- a/tests/unit/work/test_result.py +++ b/tests/unit/work/test_result.py @@ -21,7 +21,14 @@ import pytest -from neo4j import Record +from neo4j import ( + Address, + Record, + ResultSummary, + ServerInfo, + SummaryCounters, + Version, +) from neo4j.data import DataHydrator from neo4j.work.result import Result @@ -71,13 +78,16 @@ def __eq__(self, other): def __repr__(self): return "Message(%s)" % self.message - def __init__(self, records=None): + def __init__(self, records=None, run_meta=None, summary_meta=None): self._records = records self.fetch_idx = 0 self.record_idx = 0 self.to_pull = None self.queued = [] self.sent = [] + self.run_meta = run_meta + self.summary_meta = summary_meta + ConnectionStub.server_info.update({"server": "Neo4j/4.3.0"}) def send_all(self): self.sent += self.queued @@ -89,11 +99,13 @@ def fetch_message(self): msg = self.sent[self.fetch_idx] if msg == "RUN": self.fetch_idx += 1 - msg.on_success({"fields": self._records.fields}) + msg.on_success({"fields": self._records.fields, + **(self.run_meta or {})}) elif msg == "DISCARD": self.fetch_idx += 1 self.record_idx = len(self._records) - msg.on_success() + msg.on_success(self.summary_meta or {}) + msg.on_summary() elif msg == "PULL": if self.to_pull is None: n = msg.kwargs.get("n", -1) @@ -113,7 +125,8 @@ def fetch_message(self): if self.record_idx < len(self._records): msg.on_success({"has_more": True}) else: - msg.on_success({"bookmark": "foo"}) + msg.on_success({"bookmark": "foo", + **(self.summary_meta or {})}) msg.on_summary() def run(self, *args, **kwargs): @@ -125,7 +138,7 @@ def discard(self, *args, **kwargs): def pull(self, *args, **kwargs): self.queued.append(ConnectionStub.Message("PULL", *args, **kwargs)) - server_info = "ServerInfo" + server_info = ServerInfo(Address(("bolt://localhost", 7687)), Version(4, 3)) def defunct(self): return False @@ -142,11 +155,11 @@ def noop(*_, **__): def test_result_iteration(): records = [[1], [2], [3], [4], [5]] - connection = ConnectionStub(records=Records("x", records)) + connection = ConnectionStub(records=Records(["x"], records)) result = Result(connection, HydratorStub(), 2, noop, noop) result._run("CYPHER", {}, None, "r", None) received = [] - for i, record in enumerate(result): + for record in result: assert isinstance(record, Record) received.append([record.data().get("x", None)]) assert received == records @@ -154,7 +167,7 @@ def test_result_iteration(): def test_result_next(): records = [[1], [2], [3], [4], [5]] - connection = ConnectionStub(records=Records("x", records)) + connection = ConnectionStub(records=Records(["x"], records)) result = Result(connection, HydratorStub(), 2, noop, noop) result._run("CYPHER", {}, None, "r", None) iter_ = iter(result) @@ -169,7 +182,7 @@ def test_result_next(): @pytest.mark.parametrize("records", ([[1], [2]], [[1]], [])) @pytest.mark.parametrize("fetch_size", (1, 2)) def test_result_peek(records, fetch_size): - connection = ConnectionStub(records=Records("x", records)) + connection = ConnectionStub(records=Records(["x"], records)) result = Result(connection, HydratorStub(), fetch_size, noop, noop) result._run("CYPHER", {}, None, "r", None) record = result.peek() @@ -183,7 +196,7 @@ def test_result_peek(records, fetch_size): @pytest.mark.parametrize("records", ([[1], [2]], [[1]], [])) @pytest.mark.parametrize("fetch_size", (1, 2)) def test_result_single(records, fetch_size): - connection = ConnectionStub(records=Records("x", records)) + connection = ConnectionStub(records=Records(["x"], records)) result = Result(connection, HydratorStub(), fetch_size, noop, noop) result._run("CYPHER", {}, None, "r", None) with pytest.warns(None) as warning_record: @@ -198,3 +211,92 @@ def test_result_single(records, fetch_size): assert not warning_record assert isinstance(record, Record) assert record.get("x") == records[0][0] + + +def test_keys_are_available_before_and_after_stream(): + connection = ConnectionStub(records=Records(["x"], [[1], [2]])) + result = Result(connection, HydratorStub(), 1, noop, noop) + result._run("CYPHER", {}, None, "r", None) + assert list(result.keys()) == ["x"] + list(result) + assert list(result.keys()) == ["x"] + + +@pytest.mark.parametrize("records", ([[1], [2]], [[1]], [])) +@pytest.mark.parametrize("consume_one", (True, False)) +@pytest.mark.parametrize("summary_meta", (None, {"database": "foobar"})) +def test_consume(records, consume_one, summary_meta): + connection = ConnectionStub(records=Records(["x"], records), + summary_meta=summary_meta) + result = Result(connection, HydratorStub(), 1, noop, noop) + result._run("CYPHER", {}, None, "r", None) + if consume_one: + try: + next(iter(result)) + except StopIteration: + pass + summary = result.consume() + assert isinstance(summary, ResultSummary) + if summary_meta and "db" in summary_meta: + assert summary.database == summary_meta["db"] + else: + assert summary.database is None + server_info = summary.server + assert isinstance(server_info, ServerInfo) + assert server_info.version_info() == Version(4, 3) + assert server_info.protocol_version == Version(4, 3) + assert isinstance(summary.counters, SummaryCounters) + + +@pytest.mark.parametrize("t_first", (None, 0, 1, 123456789)) +@pytest.mark.parametrize("t_last", (None, 0, 1, 123456789)) +def test_time_in_summary(t_first, t_last): + run_meta = None + if t_first is not None: + run_meta = {"t_first": t_first} + summary_meta = None + if t_last is not None: + summary_meta = {"t_last": t_last} + connection = ConnectionStub(records=Records(["n"], + [[i] for i in range(100)]), + run_meta=run_meta, summary_meta=summary_meta) + + result = Result(connection, HydratorStub(), 1, noop, noop) + result._run("CYPHER", {}, None, "r", None) + summary = result.consume() + + if t_first is not None: + assert isinstance(summary.result_available_after, int) + assert summary.result_available_after == t_first + else: + assert summary.result_available_after is None + if t_last is not None: + assert isinstance(summary.result_consumed_after, int) + assert summary.result_consumed_after == t_last + else: + assert summary.result_consumed_after is None + assert not hasattr(summary, "t_first") + assert not hasattr(summary, "t_last") + + +def test_counts_in_summary(): + connection = ConnectionStub(records=Records(["n"], [[1], [2]])) + + result = Result(connection, HydratorStub(), 1, noop, noop) + result._run("CYPHER", {}, None, "r", None) + summary = result.consume() + + assert isinstance(summary.counters, SummaryCounters) + + +@pytest.mark.parametrize("query_type", ("r", "w", "rw", "s")) +def test_query_type(query_type): + connection = ConnectionStub(records=Records(["n"], [[1], [2]]), + summary_meta={"type": query_type}) + + result = Result(connection, HydratorStub(), 1, noop, noop) + result._run("CYPHER", {}, None, "r", None) + summary = result.consume() + + assert isinstance(summary.query_type, str) + assert summary.query_type == query_type diff --git a/tests/unit/work/test_session.py b/tests/unit/work/test_session.py index ff6c0ffa..d69819b5 100644 --- a/tests/unit/work/test_session.py +++ b/tests/unit/work/test_session.py @@ -23,8 +23,9 @@ from unittest.mock import NonCallableMagicMock from neo4j import ( + ServerInfo, Session, - SessionConfig, ServerInfo, + SessionConfig, ) @@ -193,3 +194,24 @@ def test_closes_connection_after_tx_commit(pool, test_run_args): tx.commit() assert session._connection is None assert session._connection is None + + +@pytest.mark.parametrize("bookmarks", (None, [], ["abc"], ["foo", "bar"])) +def test_session_returns_bookmark_directly(pool, bookmarks): + with Session(pool, SessionConfig(bookmarks=bookmarks)) as session: + if bookmarks: + assert session.last_bookmark() == bookmarks[-1] + else: + assert session.last_bookmark() is None + + +@pytest.mark.parametrize(("query", "error_type"), ( + (None, ValueError), + (1234, TypeError), + ({"how about": "no?"}, TypeError), + (["I don't", "think so"], TypeError), +)) +def test_session_run_wrong_types(pool, query, error_type): + with Session(pool, SessionConfig()) as session: + with pytest.raises(error_type): + session.run(query)