foundationdb/bindings/python/tests/unit_tests.py

306 lines
8.8 KiB
Python

#!/usr/bin/python
#
# unit_tests.py
#
# This source file is part of the FoundationDB open source project
#
# Copyright 2013-2026 Apple Inc. and the FoundationDB project authors
#
# 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 argparse
import sys
import time
import traceback
import json
import fdb
if __name__ == "__main__":
fdb.api_version(fdb.LATEST_API_VERSION)
from cancellation_timeout_tests import test_timeouts
from cancellation_timeout_tests import test_db_timeouts
from cancellation_timeout_tests import test_cancellation
from cancellation_timeout_tests import test_retry_limits
from cancellation_timeout_tests import test_db_retry_limits
from cancellation_timeout_tests import test_combinations
from size_limit_tests import test_size_limit_option, test_get_approximate_size
VERBOSE = False
def log(msg):
if VERBOSE:
print(msg, file=sys.stderr, flush=True)
def test_fdb_transactional_generator(db):
try:
@fdb.transactional
def function_that_yields(tr):
yield 0
assert (
fdb.get_api_version() < 630
), "Pre-6.3, a decorator may wrap a function that yields"
except ValueError:
assert (
fdb.get_api_version() >= 630
), "Post-6.3, a decorator should throw if wrapped function yields"
def test_fdb_transactional_returns_generator(db):
try:
def function_that_yields(tr):
yield 0
@fdb.transactional
def function_that_returns(tr):
return function_that_yields(tr)
function_that_returns()
assert fdb.get_api_version() < 630, "Pre-6.3, returning a generator is allowed"
except ValueError:
assert (
fdb.get_api_version() >= 630
), "Post-6.3, returning a generator should throw"
def test_db_options(db):
db.options.set_location_cache_size(100001)
db.options.set_max_watches(100001)
db.options.set_datacenter_id("dc_id")
db.options.set_machine_id("machine_id")
db.options.set_snapshot_ryw_enable()
db.options.set_snapshot_ryw_disable()
db.options.set_transaction_logging_max_field_length(1000)
db.options.set_transaction_timeout(100000)
db.options.set_transaction_timeout(0)
db.options.set_transaction_timeout(0)
db.options.set_transaction_max_retry_delay(100)
db.options.set_transaction_size_limit(100000)
db.options.set_transaction_retry_limit(10)
db.options.set_transaction_retry_limit(-1)
db.options.set_transaction_causal_read_risky()
db.options.set_transaction_include_port_in_address()
@fdb.transactional
def test_options(tr):
tr.options.set_priority_system_immediate()
tr.options.set_priority_batch()
tr.options.set_causal_read_risky()
tr.options.set_causal_write_risky()
tr.options.set_read_your_writes_disable()
tr.options.set_read_system_keys()
tr.options.set_access_system_keys()
tr.options.set_transaction_logging_max_field_length(1000)
tr.options.set_timeout(60 * 1000)
tr.options.set_retry_limit(50)
tr.options.set_max_retry_delay(100)
tr.options.set_used_during_commit_protection_disable()
tr.options.set_debug_transaction_identifier("my_transaction")
tr.options.set_log_transaction()
tr.options.set_read_lock_aware()
tr.options.set_lock_aware()
tr.options.set_include_port_in_address()
tr.get(b"\xff").wait()
def check_watches(db, watches, expected):
for i, watch in enumerate(watches):
if watch.is_ready() or expected:
try:
watch.wait()
if not expected:
assert False, "Watch %d is ready" % i
except fdb.FDBError as e:
tr = db.create_transaction()
tr.on_error(e).wait()
return False
return True
def test_watches(db):
while True:
db[b"w0"] = b"0"
db[b"w3"] = b"3"
watches = [None]
@fdb.transactional
def txn1(tr):
watches[0] = tr.watch(b"w0")
tr.set(b"w0", b"0")
assert not watches[0].is_ready()
txn1(db)
watches.append(db.clear_and_watch(b"w1"))
watches.append(db.set_and_watch(b"w2", b"2"))
watches.append(db.get_and_watch(b"w3"))
assert watches[3][0] == b"3"
watches[3] = watches[3][1]
time.sleep(1)
if not check_watches(db, watches, False):
continue
del db[b"w1"]
time.sleep(5)
if not check_watches(db, watches, False):
continue
db[b"w0"] = b"a"
db[b"w1"] = b"b"
del db[b"w2"]
db.bit_xor(b"w3", b"\xff\xff")
if check_watches(db, watches, True):
return
@fdb.transactional
def test_locality(tr):
tr.options.set_timeout(60 * 1000)
tr.options.set_read_system_keys() # We do this because the last shard (for now, someday the last N shards) is in the /FF/ keyspace
# This isn't strictly transactional, thought we expect it to be given the size of our database
boundary_keys = list(fdb.locality.get_boundary_keys(tr, b"", b"\xff\xff")) + [
b"\xff\xff"
]
end_keys = [
tr.get_key(fdb.KeySelector.last_less_than(k)) for k in boundary_keys[1:]
]
start_addresses = [
fdb.locality.get_addresses_for_key(tr, k) for k in boundary_keys[:-1]
]
end_addresses = [fdb.locality.get_addresses_for_key(tr, k) for k in end_keys]
if [set(s.wait()) for s in start_addresses] != [
set(e.wait()) for e in end_addresses
]:
raise Exception("Locality not internally consistent.")
def test_predicates():
assert fdb.predicates.is_retryable(fdb.FDBError(1020))
assert not fdb.predicates.is_retryable(fdb.FDBError(10))
def test_get_client_status(db):
@fdb.transactional
def simple_txn(tr):
tr.get_read_version().wait()
# Execute a simple transaction
# to make sure the database is initialized
simple_txn(db)
# Here we just check if a meaningful client report status is returned
# Different report attributes and error cases are covered by C API tests
status_str = db.get_client_status().wait()
status = json.loads(status_str)
assert "Healthy" in status
assert status["Healthy"]
def test_range_split_points(db):
begin = b"\x02range-split-points-a"
end = b"\x02range-split-points-z"
tr = db.create_transaction()
for limit in (-1, 0, 1, 2):
split_points = tr.get_range_split_points(begin, end, 1000000, limit).wait()
assert split_points[0] == begin
assert split_points[-1] == end
if limit >= 0:
assert len(split_points) <= limit + 2
def run_unit_tests(db):
try:
log("test_db_options")
test_db_options(db)
log("test_options")
test_options(db)
log("test_watches")
test_watches(db)
log("test_cancellation")
test_cancellation(db)
log("test_retry_limits")
test_retry_limits(db)
log("test_db_retry_limits")
test_db_retry_limits(db)
log("test_timeouts")
test_timeouts(db)
log("test_db_timeouts")
test_db_timeouts(db)
log("test_combinations")
test_combinations(db)
log("test_locality")
test_locality(db)
log("test_predicates")
test_predicates()
log("test_size_limit_option")
test_size_limit_option(db)
log("test_get_approximate_size")
test_get_approximate_size(db)
log("test_get_client_status")
test_get_client_status(db)
log("test_range_split_points")
test_range_split_points(db)
except fdb.FDBError as e:
print("Unit tests failed: %s" % e.description)
traceback.print_exc()
raise Exception("Unit tests failed: %s" % e.description)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
Unit tests for python FDB API.
""",
)
parser.add_argument(
"--cluster-file",
"-C",
help="FDB cluster file",
required=True,
)
parser.add_argument(
"--verbose",
"-V",
help="Print diagnostic info",
action="store_true",
)
args = parser.parse_args()
if args.verbose:
VERBOSE = True
log("Opening database {}".format(args.cluster_file))
db = fdb.open(args.cluster_file)
run_unit_tests(db)