Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions bindings/python/src/arcadedb_embedded/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,24 @@ def to_java_int_array(vector):
vector: Array-like object containing integer values

Returns:
Java int array
Java int array (a NumPy int32 array on the native backend, which has no JVM to build one in: like
:func:`to_java_float_array`, it binds as a SQL parameter in place of the Java array)
"""
if _native_enabled():
values = (
vector if isinstance(vector, _np.ndarray) else _np.asarray(list(vector))
)
if values.size == 0:
return values.astype(_np.int32)
if values.dtype.kind not in "iu":
raise TypeError("to_java_int_array takes integer values")
bounds = _np.iinfo(_np.int32)
if values.min() < bounds.min or values.max() > bounds.max:
raise OverflowError(
"Cannot convert value to Java int: an element does not fit in 32 bits"
)
return values.astype(_np.int32)

if _np is not None and isinstance(vector, _np.ndarray):
if vector.dtype.kind in "iu" and vector.size:
bounds = _np.iinfo(_np.int32)
Expand Down Expand Up @@ -125,8 +141,29 @@ def to_java_byte_array(vector):
vector: Array-like object containing byte values

Returns:
Java byte array compatible with INT8-encoded ArcadeDB vector indexes
Java byte array compatible with INT8-encoded ArcadeDB vector indexes (a NumPy int8 array on the native
backend, which has no JVM to build one in)
"""
if _native_enabled():
if isinstance(vector, (bytes, bytearray, memoryview)):
return _np.frombuffer(bytes(vector), dtype=_np.int8).copy()
if (
isinstance(vector, _np.ndarray)
and vector.dtype.kind in "iu"
and vector.dtype.itemsize == 1
):
return _np.ascontiguousarray(vector).view(_np.int8).copy()
native_bytes = []
for value in vector.tolist() if isinstance(vector, _np.ndarray) else vector:
byte_value = int(value)
if byte_value < -128 or byte_value > 255:
raise ArcadeDBError(
"Byte vectors must contain values in the signed byte range "
"[-128, 127] or unsigned byte range [0, 255]"
)
native_bytes.append(byte_value - 256 if byte_value > 127 else byte_value)
return _np.asarray(native_bytes, dtype=_np.int8)

try:
import numpy as np

Expand Down
184 changes: 108 additions & 76 deletions bindings/python/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,85 +54,135 @@
collect_ignore.extend(NATIVE_NOT_APPLICABLE)


# Failure texts that mean the test is about the JPype bridge, not about database behaviour.
NATIVE_JVM_MARKERS = (
"JVMNotRunning",
"without JVM",
"Java Virtual Machine is not running",
"'_java_db'",
"'_java_result'",
"bridge jar",
"'nextRows'",
"getClass",
"DID NOT RAISE KeyboardInterrupt",
"Cannot drop the database in transaction", # teardown after an injected KeyboardInterrupt
"assert (None is not None)", # fixtures that need a bridge class
"KeyError: 'flush'",
"KeyError: 'max_connections'",
"cause is <class 'NoneType'>", # the Java exception as __cause__
"findNeighborsFromVector", # the test calls the Java vector index directly
"to_java_rid",
# Tests that exercise the JVM itself, so the native backend (no JVM, no jars) has nothing to run. Skipped before they start, at
# collection time, with the reason (reported as "NA-JVM: <why>"). The key is "<file>::[<Class>::]<test function>": every parameter
# of the function, or one parameter id as in "test_x.py::test_y[java]". A test is listed only when it reaches into the JVM on purpose:
# it builds or reads a Java object (jpype.JClass, a Java array, db._java_db, _java_index, the bridge classes), counts JPype bridge
# crossings, or patches a hook of the JPype path. A failure text never puts a test on this list, and a failing test is never turned
# into a skip: a test whose behaviour native shares has to pass on native, and one that fails there is a bug to file (an
# xfail(strict=True) naming the BACKLOG-bindings.md row, so it turns red when fixed), not an entry here.
# A feature the native wheel lacks on purpose raises NotOnNativeYet from the native code, which pytest_runtest_makereport reports as NOTYET.
JAVA_CLASS = "builds a java.* object through JPype"
JAVA_ARRAY = "builds a Java array through JPype (jpype.JArray)"
JAVA_DB = "reads the Java database behind the wrapper (db._java_db)"
JAVA_INDEX = "reads the Java index behind the wrapper (_java_index: getClass, metadata)"
BRIDGE_COUNT = (
"counts or intercepts the JPype bridge crossings (an internal of the JPype path)"
)


# Failure texts that mean the test needs a Java-object wrapper (Document, Vertex, Edge) or the exporter's Java path.
NATIVE_NOTYET_MARKERS = (
"'NativeResult' object is not iterable",
"assert 'NativeResult' ==",
)


# Tests that count the JPype parameter conversions (an internal of the JPype path).
NATIVE_NA_TESTS = {
"test_non_scalars_keep_the_general_path",
"test_a_subclass_of_a_scalar_is_not_assumed_to_be_one",
"test_lsm_vector_search_looks_up_all_hits_in_one_bridge_call", # counts GraphCalls bridge crossings
NATIVE_NA_JVM_TESTS = {
# JVM start-up, signals and settings
"test_sigint.py::test_ctrl_c_raises_keyboard_interrupt_and_runs_cleanup[java]": "a Java call blocked in JPype's SIGINT handler (#179)",
"test_sigint.py::test_interrupt_true_keeps_the_jvm_handling_sigint": "start_jvm(interrupt=True): the JVM handles SIGINT",
"test_docs_examples.py::test_docs_installation_jvm_examples": "documents start_jvm() and jvm_kwargs",
"test_server_wire_protocols.py::test_the_host_reaches_exactly_the_listener_bind_settings": "reads the engine's GlobalConfiguration through the JVM",
"test_server_wire_protocols.py::test_wire_listeners_stay_on_loopback_by_default": "reads the server's GlobalConfiguration through the JVM",
"test_server_wire_protocols.py::test_host_applies_to_every_wire_listener": "reads the server's GlobalConfiguration through the JVM",
"test_server_wire_protocols.py::test_a_protocol_host_overrides_the_server_host_for_that_protocol_only": "reads the server's GlobalConfiguration through the JVM",
"test_sql_constraints_and_maintenance.py::test_backup_check_and_rebuild_index_statements": "sets GlobalConfiguration.SERVER_BACKUP_DIRECTORY through the JVM",
"test_vector_params_verification.py::TestVectorParams::test_jvm_heap_check": "reads java.lang.Runtime through JPype",
"test_vector_params_verification.py::TestVectorParams::test_wrapper_default_matches_engine_default": "reads the engine default through java.lang.String and GlobalConfiguration",
"test_vector_params_verification.py::TestVectorParams::test_omitted_max_connections_reaches_the_index": "reads the engine default through java.lang.String and GlobalConfiguration",
"test_vector_sql.py::TestVectorSearchOptionsSql::test_where_map_and_condition": "picks the expected behaviour by com.arcadedb.Constants.getRawVersion() through JPype",
"test_vector_sql.py::TestVectorSearchOptionsSql::test_where_combines_with_filter": "picks the expected behaviour by com.arcadedb.Constants.getRawVersion() through JPype",
# Java objects built or read by the test
"test_conversion_fixes.py::test_java_instants_convert_without_losing_microseconds": JAVA_CLASS,
"test_type_conversion.py::test_offset_datetime_conversion": JAVA_CLASS,
"test_type_conversion.py::test_scalar_list_crosses_as_one_array_with_the_same_types": "asserts the converted value is a java.util.ArrayList",
"test_type_conversion.py::TestPrimitiveArrayFormats::test_int_array": JAVA_ARRAY,
"test_type_conversion.py::TestPrimitiveArrayFormats::test_long_array": JAVA_ARRAY,
"test_percall_calls.py::test_float_array_conversion_gives_a_float_array": 'asserts the converted value is a Java float[] (getClass() == "[F")',
"test_typed_rows.py::test_java_values_sql_cannot_produce": JAVA_CLASS,
"test_typed_rows.py::test_value_python_cannot_hold_raises_as_before": JAVA_CLASS,
"test_typed_rows.py::test_object_array_raises_as_before": JAVA_ARRAY,
"test_typed_rows.py::test_iter_dicts_reads_one_row_at_a_time": "builds a Java InternalResultSet",
"test_typed_rows.py::test_vertex_edges_match_the_engine_iteration": "the oracle is the Java Vertex.getEdges iteration",
"test_typed_rows.py::test_vector_search_hits_match_the_per_hit_path": "the oracle is the Java vector index (findNeighborsFromVector)",
"test_typed_rows.py::test_a_batch_of_wide_rows_ends_at_a_few_megabytes": "calls the bridge's typed_rows.nextRows on the Java result set",
"test_typed_rows.py::test_the_batch_says_whether_the_result_set_is_drained": "calls the bridge's typed_rows.nextRows on the Java result set",
"test_graph_api.py::test_document_wrap_static_method": "wraps the Java object behind a result (_java_result)",
"test_legacy_temporal_implementations.py::test_the_engine_hands_back_the_configured_class_holding_the_utc_instant": JAVA_DB,
"test_graph_batch.py::test_graph_batch_leaves_the_callers_wal_setting_alone": JAVA_DB,
"test_transaction_config.py::test_set_wal_flush_is_database_wide": JAVA_DB,
"test_run_in_transaction_retry.py::test_contended_commits_spend_their_budget_before_failing": JAVA_DB,
# Java exceptions thrown and engine hooks set from the test
"test_run_in_transaction_retry.py::test_every_need_retry_exception_is_retried": "throws Java exceptions (com.arcadedb.exception.*) from the test",
"test_run_in_transaction_retry.py::test_the_budget_is_spent_before_the_conflict_reaches_the_caller": "throws Java exceptions (com.arcadedb.exception.*) from the test",
"test_run_in_transaction_retry.py::test_the_wait_the_refusing_side_asked_for_is_honored": "throws Java exceptions (com.arcadedb.exception.*) from the test",
"test_run_in_transaction_retry.py::test_the_wait_is_capped_by_retry_after_max_wait": "throws Java exceptions (com.arcadedb.exception.*) from the test",
"test_run_in_transaction_retry.py::test_a_block_that_committed_part_of_its_work_is_not_replayed": "throws a Java ConcurrentModificationException from the test (native: test_run_in_transaction_partial_commit.py)",
"test_graph_batch.py::test_graph_batch_outside_the_callers_transactions": "sets GraphBatch.TEST_BEFORE_VERTEX_COMMIT_HOOK, a Java static, through JPype",
"test_graph_batch.py::test_graph_batch_create_vertices_refuses_before_any_commit_attempt": "sets GraphBatch.TEST_BEFORE_VERTEX_COMMIT_HOOK, a Java static, through JPype",
"test_graph_batch.py::test_graph_batch_create_vertices_keyboard_interrupt_rolls_back": "patches GraphBatch._create_vertices_json_bulk, the JPype path's bulk call",
# parameter conversion and bridge crossings of the JPype path
"test_parameter_binding.py::test_each_parameter_shape_reaches_one_java_overload": "inspects the Java Database overloads through JPype",
"test_parameter_binding.py::test_async_parameters_reach_one_java_overload": "inspects the Java DatabaseAsyncExecutor overloads through JPype",
"test_parameter_binding.py::TestScalarParametersTakeTheShortPath::test_a_java_array_value_is_not_converted_and_binds_as_before": JAVA_ARRAY,
"test_parameter_binding.py::TestScalarParametersTakeTheShortPath::test_non_scalars_keep_the_general_path": "counts JPype parameter conversions",
"test_parameter_binding.py::TestScalarParametersTakeTheShortPath::test_a_subclass_of_a_scalar_is_not_assumed_to_be_one": "counts JPype parameter conversions",
"test_resultset.py::test_to_dict_one_crossing_matches_the_per_property_path": "needs the bridge jar's RowAccess class",
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_to_list_makes_one_call_for_a_short_result": BRIDGE_COUNT,
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_to_list_of_an_empty_result_makes_one_call": BRIDGE_COUNT,
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_to_json_list_makes_one_call_for_a_short_result": BRIDGE_COUNT,
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_a_full_batch_still_ends_on_the_next_call": BRIDGE_COUNT,
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_a_drained_set_reads_as_empty_and_is_not_an_error": BRIDGE_COUNT,
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_python_does_not_cross_the_bridge_to_close_a_drained_set": BRIDGE_COUNT,
"test_resultset.py::TestSmallResultsCostOneBridgeCall::test_the_bridge_closes_what_it_drains_and_only_that": BRIDGE_COUNT,
"test_vector.py::TestLSMVectorIndex::test_lsm_vector_search_looks_up_all_hits_in_one_bridge_call": BRIDGE_COUNT,
"test_vector.py::TestLSMVectorIndex::test_lsm_vector_search_uses_database_rid_conversion": "intercepts Database.to_java_rid, the JPype path's RID conversion",
# the Java index behind the wrapper
"test_vector.py::TestLSMVectorIndex::test_create_vector_index_with_pq_params": JAVA_INDEX,
"test_vector.py::TestLSMVectorIndex::test_create_vector_index_with_custom_id_property": JAVA_INDEX,
"test_vector.py::TestLSMVectorIndex::test_lsm_vector_search_approximate_product": JAVA_INDEX,
"test_vector.py::TestLSMVectorIndex::test_lsm_vector_search_approximate_typeindex": JAVA_INDEX,
**{
f"test_vector_params_verification.py::TestVectorParams::{name}": JAVA_INDEX
for name in (
"test_quantization_param",
"test_store_vectors_in_graph_param",
"test_encoding_param",
"test_add_hierarchy_param",
"test_per_index_cache_params",
"test_quantization_none",
"test_quantization_binary",
"test_quantization_product",
)
},
}


# Tests that exercise the JVM itself, so the native backend (no JVM, no jars) has nothing to run. Skipped before they start,
# with the reason; a test whose behaviour native shares (Ctrl-C in a Python loop, the zone of to_json_list) is not listed.
NATIVE_NA_JVM_TESTS = {
"test_ctrl_c_raises_keyboard_interrupt_and_runs_cleanup[java]": "a Java call blocked in JPype's SIGINT handler (#179)",
"test_interrupt_true_keeps_the_jvm_handling_sigint": "start_jvm(interrupt=True): the JVM handles SIGINT",
"test_docs_installation_jvm_examples": "documents start_jvm() and jvm_kwargs",
"test_to_java_int_array_keeps_values_that_fit": "to_java_int_array builds a Java int[] (JPype)",
"test_to_java_int_array_refuses_values_beyond_32_bits_in_an_array": "to_java_int_array builds a Java int[] (JPype)",
"test_to_java_byte_array_accepts_bytes_like_fast_paths": "to_java_byte_array builds a Java byte[] (JPype)",
"test_to_java_int_array": "to_java_int_array builds a Java int[] (JPype)",
"test_to_java_int_array_accepts_numpy_directly": "to_java_int_array builds a Java int[] (JPype)",
"test_to_java_int_array_round_trips_through_a_sparse_vector": "to_java_int_array builds a Java int[] (JPype)",
"test_the_host_reaches_exactly_the_listener_bind_settings": "reads the engine's GlobalConfiguration through the JVM",
}
def _native():
return os.environ.get("ARCADEDB_BACKEND", "jpype").lower() == "native"


def pytest_collection_modifyitems(config, items):
if os.environ.get("ARCADEDB_BACKEND", "jpype").lower() != "native":
if not _native():
return
for item in items:
reason = NATIVE_NA_JVM_TESTS.get(item.name)
# "<file>::[<Class>::]<function>[<param id>]", the file without its directory
test_id = f"{item.path.name}::{item.nodeid.split('::', 1)[-1]}"
reason = NATIVE_NA_JVM_TESTS.get(test_id) or NATIVE_NA_JVM_TESTS.get(
test_id.split("[", 1)[0]
)
if reason:
item.add_marker(pytest.mark.skip(reason=f"NA-JVM: {reason}"))


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""On the native backend, report a test as NOTYET when the native code raised NotOnNativeYet.

That is the one explicit signal: the native code path names a feature the wheel does not have (yet) by raising a dedicated
exception, the exception itself or an exception raised from it. Nothing else is turned into a skip. In particular no failure text
is matched: a text such as "assert None is not None" or "KeyError: 'flush'" is also what a real bug looks like.
"""
outcome = yield
if os.environ.get("ARCADEDB_BACKEND", "jpype").lower() != "native":
if not _native():
return
report = outcome.get_result()
if call.excinfo is None or report.passed:
return
from arcadedb_embedded._native import NotOnNativeYet

if item.name in NATIVE_NA_TESTS:
report.outcome = "skipped"
report.longrepr = (
str(item.fspath),
item.location[1] or 0,
"NA-JVM: counts JPype conversions",
)
return
exc, seen = call.excinfo.value, 0
while exc is not None and seen < 8:
if isinstance(exc, NotOnNativeYet):
Expand All @@ -143,25 +193,7 @@ def pytest_runtest_makereport(item, call):
f"NOTYET: {exc}",
)
return
text = f"{type(exc).__name__}: {exc}"
if any(marker in text for marker in NATIVE_NOTYET_MARKERS):
report.outcome = "skipped"
report.longrepr = (
str(item.fspath),
item.location[1] or 0,
"NOTYET: Java-object wrapper or exporter path",
)
return
if any(marker in text for marker in NATIVE_JVM_MARKERS):
# the test reaches into the JVM directly (jpype.JClass, Java arrays, bridge classes): about the JPype path
report.outcome = "skipped"
report.longrepr = (
str(item.fspath),
item.location[1] or 0,
"NA-JVM: the test uses the JVM directly",
)
return
exc, seen = exc.__cause__ or exc.__context__, seen + 1
exc, seen = exc.__cause__, seen + 1


@pytest.hookimpl(trylast=True)
Expand Down
Loading
Loading