#!/usr/bin/python # # python_correctness.py # # This source file is part of the FoundationDB open source project # # Copyright 2013-2018 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 os import sys import time import random import traceback sys.path.append(os.path.join(os.path.dirname(__file__), "..")) from python_tests import PythonTest import fdb import fdb.tuple fdb.api_version(400) # A class that mimics some of the operations of the FoundationDB key-value store class KeyValueStore: # Uses a simple dictionary to store key-value pairs # Any operations that depend on the order of keys first sort the data store = dict() def get(self, key): if key is fdb.KeySelector: key = self.get_key(key) if key in self.store: return self.store[key] else: return None def get_key(self, key_selector): sorted_keys = list(sorted(self.store.keys())) for index, key in enumerate(sorted_keys): if key >= key_selector.key: index += key_selector.offset if ( key == key_selector.key and not key_selector.or_equal ) or key != key_selector.key: index -= 1 if index < 0 or index >= len(sorted_keys): return "" else: return sorted_keys[index] index = len(sorted_keys) + key_selector.offset - 1 if index < 0 or index >= len(sorted_keys): return "" else: return sorted_keys[index] def get_range(self, begin, end, limit=None): values = [] count = 0 if begin is fdb.KeySelector: begin = self.get_key(begin) if end is fdb.KeySelector: end = self.get_key(end) for key, value in sorted(self.store.items()): if limit is not None and count >= limit: break if key >= end: break if key >= begin: values.append([key, value]) count += 1 return values def get_range_startswith(self, prefix, limit=None): values = [] count = 0 for key, value in sorted(self.store.items()): if limit is not None and count >= limit: break if key > prefix and not key.startswith(prefix): break if key.startswith(prefix): values.append([key, value]) count += 1 return values def set(self, key, value): self.store[key] = value def clear(self, key): if key is fdb.KeySelector: key = self.get_key(key) if key in self.store: del self.store[key] def clear_range(self, begin, end): for key, value in self.get_range(begin, end): del self.store[key] def clear_range_startswith(self, prefix): for key, value in self.get_range_startswith(prefix): del self.store[key] class PythonCorrectness(PythonTest): callback = False callbackError = "" # Python correctness tests (checks if functions run and yield correct results) def run_test(self): try: db = fdb.open(None, "DB") except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("fdb.open failed")) return try: print("Testing functions...") self.test_functions(db) print("Testing correctness...") del db[:] self.test_correctness(db) except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Failed to complete all tests")) # Generates a random set of keys and values def generate_data( self, num_keys, min_key_length, max_key_length, min_value_length, max_value_length, prefix="", allow_duplicates=True, ): data = list() keys = set() while len(data) < num_keys: # key = prefix + ''.join(random.choice(string.ascii_lowercase) # for i in range(0, random.randint(minKeyLength - len(prefix), maxKeyLength - len(prefix)))) key = prefix + "".join( chr(random.randint(0, 254)) for _ in range( 0, random.randint( min_key_length - len(prefix), max_key_length - len(prefix) ), ) ) if not allow_duplicates: if key in keys: continue else: keys.add(key) value = "".join( "x" for _ in range(0, random.randint(max_key_length, max_value_length)) ) data.append([key, value]) return data # Function to test the callback feature of Future objects def test_callback(self, future): try: future.wait() except KeyboardInterrupt: raise except Exception: self.callbackError = self.get_error("Callback future get failed") self.callback = True # Tests that all of the functions in the python API can be called without failing def test_functions(self, db): self.callback = False self.callbackError = "" try: tr = db.create_transaction() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("db.create_transaction failed")) return try: tr["testkey"] = "testvalue" value = tr["testkey"] value.is_ready() value.block_until_ready() value.wait() except KeyboardInterrupt: raise except Exception: self.result.add_error( self.get_error("Set/Get value failed (block until ready)") ) try: value = tr["testkey"] value.wait() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get value failed")) try: tr["testkey"] = "newtestvalue" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Replace value failed")) try: value = tr["fakekey"] # The following line would generate a segfault # value.capi.fdb_future_block_until_ready(0) value.wait() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get non-existent key failed")) try: tr.commit().wait() tr.get_committed_version() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Commit failed")) try: tr.reset() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Reset failed")) try: version = tr.get_read_version() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("tr.get_read_version failed")) try: value = tr["testkey"] value.wait() tr.reset() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get and reset failed")) try: tr.set_read_version(version.wait()) except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Set read version failed")) try: value = tr["testkey"] callback_time = time.time() value.on_ready(self.test_callback) except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get future and set callback failed")) try: del tr["testkey"] except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Delete key failed")) try: del tr["fakekey"] except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Delete non-existent key failed")) try: tr.set("testkey", "testvalue") value = tr.get("testkey") value.wait() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Future.get failed")) try: tr.clear("testkey") except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Clear key failed")) try: tr["testkey1"] = "testvalue1" tr["testkey2"] = "testvalue2" tr["testkey3"] = "testvalue3" for k, v in tr.get_range("testkey1", "testkey3"): v += "" for k, v in tr.get_range("testkey1", "testkey2", 2): v += "" for k, v in tr["testkey1":"testkey3"]: v += "" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get range failed")) try: tr["otherkey1"] = "othervalue1" tr["otherkey2"] = "othervalue2" for k, v in tr.get_range_startswith("testkey"): v += "" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get range starts with failed")) try: tr.clear_range_startswith("otherkey") except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Clear range starts with failed")) try: tr.clear_range("testkey1", "testkey3") except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Clear range failed")) try: tr["testkey1"] = "testvalue1" tr["testkey2"] = "testvalue2" tr["testkey3"] = "testvalue3" begin = fdb.KeySelector("testkey2", 0, 0) end = fdb.KeySelector("testkey2", 0, 1) except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Create key selector failed")) try: for k, v in tr.get_range(begin, end): v += "" for k, v in tr.get_range(begin, end, 2): v += "" for k, v in tr[begin:end]: v += "" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Get range (key selectors) failed")) try: tr.clear_range(begin, end) tr["testkey1"] = "testvalue1" tr["testkey2"] = "testvalue2" tr["testkey3"] = "testvalue3" del tr[begin:end] tr["testkey1"] = "testvalue1" tr["testkey2"] = "testvalue2" tr["testkey3"] = "testvalue3" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Clear range (key selectors) failed")) try: begin = fdb.KeySelector.last_less_than("testkey2") end = fdb.KeySelector.first_greater_or_equal("testkey2") for k, v in tr.get_range(begin, end): v += "" begin = fdb.KeySelector.last_less_or_equal("testkey2") end = fdb.KeySelector.first_greater_than("testkey2") for k, v in tr.get_range(begin, end): v += "" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Builtin key selectors failed")) try: del tr["testkey1":"testkey3"] except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Delete key range failed")) try: tr.commit().wait() tr.get_committed_version() except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Commit failed")) try: key = fdb.tuple.pack(("k1", "k2", "k3")) k_tuple = fdb.tuple.unpack(key) if k_tuple[0] != "k1" and k_tuple[1] != "k2" and k_tuple[2] != "k3": self.result.add_error( "Tuple <-> key conversion yielded incorrect results" ) except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Tuple <-> key conversion failed")) try: tr[fdb.tuple.pack(("k1", "k2"))] = "v" tr[fdb.tuple.pack(("k1", "k2", "k3"))] = "v1" tr[fdb.tuple.pack(("k1", "k2", "k3", "k4"))] = "v2" for k, v in tr[fdb.tuple.range(("k1", "k2"))]: v += "" except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Tuple get range failed")) try: tr["testint"] = "10" y = int(tr["testint"]) + 1 if y != 11: self.result.add_error("Value retrieval yielded incorrect results") except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Future value retrieval failed")) if not self.callback: time.sleep(5) if not self.callback: self.result.add_error( "Warning: Future callback not called after %f seconds" % (time.time() - callback_time) ) if len(self.callbackError) > 0: self.result.add_error(self.callbackError) # Compares a FoundationDB database with an in-memory key-value store def compare_database_to_memory(self, db, store): db_result = self.correctness_get_range_transactional(db, "\x00", "\xff") store_result = store.get_range("\x00", "\xff") return self.compare_results(db_result, store_result) # Compares result sets coming from a FoundationDB database and an in-memory key-value store def compare_results(self, db_results, store_results): if len(db_results) != len(store_results): # print 'mismatched lengths: ' + str(len(dbResults)) + ' - ' + str(len(storeResults)) return False for i in range(0, len(db_results)): # if i >= len(storeResults): # print 'mismatched key: ' + dbResults[i].key # return False if ( db_results[i].key != store_results[i][0] or db_results[i].value != store_results[i][1] ): # print 'mismatched key: ' + dbResults[i].key + ' - ' + storeResults[i][0] return False return True # Performs the same operations on a FoundationDB database and an in-memory key-value store and compares the results def test_correctness(self, db): num_keys = 5000 ratio_short_keys = 0.5 min_short_key_length = 1 max_short_key_length = 3 min_long_key_length = 1 max_long_key_length = 128 min_value_length = 1 max_value_length = 10000 max_transaction_bytes = 5000000 num_reads = 100 num_range_reads = 100 num_prefix_reads = 100 num_get_keys = 100 # num_clears = 100 num_range_clears = 10 num_prefix_clears = 10 max_keys_per_transaction = max( 1, int(max_transaction_bytes / (max_value_length + max_long_key_length)) ) try: store = KeyValueStore() # Generate some random data data = self.generate_data( num_keys * ratio_short_keys, min_short_key_length, max_short_key_length, min_value_length, max_value_length, ) data.extend( self.generate_data( num_keys * (1 - ratio_short_keys), min_long_key_length, max_long_key_length, min_value_length, max_value_length, ) ) # Insert the data self.correctness_set(db, store, data, max_keys_per_transaction) if not self.compare_database_to_memory(db, store): self.result.add_error("transaction.set resulted in incorrect database") # Compare the results of single key reads if not self.correctness_get( db, store, data, num_reads, max_keys_per_transaction ): self.result.add_error("transaction.get returned incorrect result") # Compare the results of range reads for i in range(0, num_range_reads): if not self.correctness_get_range(db, store, data): self.result.add_error( "transaction.get_range returned incorrect results" ) break # Compare the results of prefix reads for i in range(0, num_prefix_reads): if not self.correctness_get_prefix(db, store, data): self.result.add_error( "transaction.get_range_startswith returned incorrect results" ) break # Compare the results of get key if not self.correctness_get_key( db, store, data, num_get_keys, max_keys_per_transaction ): self.result.add_error("transaction.get_key returned incorrect results") # Compare the results of clear # clearedKeys = self.correctnessClear(db, store, data, num_clears, max_keys_per_transaction) if not self.compare_database_to_memory(db, store): self.result.add_error( "transaction.clear resulted in incorrect database" ) # for key in clearedKeys: # print 'clearing key ' + key # else: # print 'successful compare' # Fill the database back up with data self.correctness_set(db, store, data, max_keys_per_transaction) if not self.compare_database_to_memory(db, store): self.result.add_error("transaction.set resulted in incorrect database") # Compare the results of clear_range for i in range(0, num_range_clears): self.correctness_clear_range(db, store, data) success = self.compare_database_to_memory(db, store) if not success: self.result.add_error( "transaction.clear_range resulted in incorrect database" ) # Fill the database back up with data self.correctness_set(db, store, data, max_keys_per_transaction) if not self.compare_database_to_memory(db, store): self.result.add_error( "transaction.set resulted in incorrect database" ) break if not success: break # Compare the results of clear_range_startswith self.correctness_clear_prefix(db, store, data, num_prefix_clears) if not self.compare_database_to_memory(db, store): self.result.add_error( "transaction.clear_range_startswith resulted in incorrect database" ) except KeyboardInterrupt: raise except Exception: self.result.add_error(self.get_error("Database error in correctness test")) # Stores data in the database and a memory key-value store def correctness_set(self, db, store, data, max_keys_per_transaction): for [key, value] in data: store.set(key, value) keys_committed = 0 while keys_committed < len(data): self.correctness_set_transactional( db, data[keys_committed : keys_committed + max_keys_per_transaction] ) keys_committed += max_keys_per_transaction # Stores data in the database @fdb.transactional def correctness_set_transactional(self, tr, data): for [key, value] in data: tr.set(key, value) # Compares the results of the get operation from the database and a memory key-value store def correctness_get(self, db, store, data, num_reads, max_keys_per_transaction): keys = [] for i in range(0, num_reads): index = random.randint(0, len(data) - 1) keys.append(data[index][0]) keys_retrieved = 0 while keys_retrieved < len(keys): sub_keys = keys[keys_retrieved : keys_retrieved + max_keys_per_transaction] values = self.correctness_get_transactional(db, sub_keys) for i in range(0, num_reads): if values[i] != store.get(sub_keys[i]): print("mismatch: %s", sub_keys[i]) return False keys_retrieved += max_keys_per_transaction return True # Gets the values for the specified list of keys from the database @fdb.transactional def correctness_get_transactional(self, tr, keys): futures = [] for key in keys: futures.append(tr.get(key)) values = [] for future in futures: values.append(future.wait()) return values # Compares the results of the get_range operation from the database and a memory key-value store def correctness_get_range(self, db, store, data): index = random.randint(0, len(data) - 1) index2 = random.randint(0, len(data) - 1) key1 = min(data[index][0], data[index2][0]) key2 = max(data[index][0], data[index2][0]) db_results = self.correctness_get_range_transactional(db, key1, key2, data) store_results = store.get_range(key1, key2) return self.compare_results(db_results, store_results) # Gets the entries in the range [key1,key2) from the database @fdb.transactional def correctness_get_range_transactional(self, tr, key1, key2, data=None): if data is not None: return list(tr.get_range(key1, key2, len(data))) else: return list(tr.get_range(key1, key2)) # Compares the results of the get_range_startswith operation from the database and a memory key-value store def correctness_get_prefix(self, db, store, data): prefix = "".join( chr(random.randint(0, 254)) for _ in range(0, random.randint(1, 3)) ) db_results = self.correctness_get_prefix_transactional(db, prefix) store_results = store.get_range_startswith(prefix) return self.compare_results(db_results, store_results) # Gets the entries with a given prefix from the database @fdb.transactional def correctness_get_prefix_transactional(self, tr, prefix): return list(tr.get_range_startswith(prefix)) # Compares the results of the get_key operation from the database and a memory key-value store def correctness_get_key( self, db, store, data, num_get_keys, max_keys_per_transaction ): selectors = [] for i in range(0, num_get_keys): index = random.randint(0, len(data) - 1) or_equal = random.randint(0, 1) offset = random.randint( max(-index + (1 - or_equal), -10), min(len(data) - index - 1 + or_equal, 10), ) key = sorted(data)[index][0] selector = fdb.KeySelector(key, or_equal, offset) selectors.append(selector) keys_retrieved = 0 while keys_retrieved < len(selectors): sub_selectors = selectors[ keys_retrieved : keys_retrieved + max_keys_per_transaction ] db_keys = self.correctness_get_key_transactional(db, sub_selectors) for i in range(0, num_get_keys): if db_keys[i] != store.get_key(sub_selectors[i]): return False keys_retrieved += max_keys_per_transaction return True # Gets the keys specified by the list of key selectors @fdb.transactional def correctness_get_key_transactional(self, tr, key_selectors): futures = [] count = 0 for selector in key_selectors: futures.append(tr.get_key(selector)) count += 1 keys = [] count = 0 for future in futures: keys.append(future.wait()) count += 1 return keys # Clears data from a database and a memory key-value store def correctness_clear(self, db, store, data, num_clears, max_keys_per_transaction): cleared_keys = [] for i in range(0, num_clears): index = random.randint(0, len(data) - 1) cleared_keys.append(data[index][0]) store.clear(data[index][0]) keys_cleared = 0 while keys_cleared < len(cleared_keys): self.correctness_clear_transactional( db, cleared_keys[keys_cleared : keys_cleared + max_keys_per_transaction] ) keys_cleared += max_keys_per_transaction return cleared_keys # Clears a list of keys from the database @fdb.transactional def correctness_clear_transactional(self, tr, cleared_keys): for key in cleared_keys: tr.clear(key) # Clears a range of data from a database and a memory key-value store def correctness_clear_range(self, db, store, data): index = random.randint(0, len(data) - 1) index2 = random.randint(0, len(data) - 1) key1 = min(data[index][0], data[index2][0]) key2 = max(data[index][0], data[index2][0]) self.correctness_clear_range_transactional(db, key1, key2) store.clear_range(key1, key2) # Clears a range of memory from a database @fdb.transactional def correctness_clear_range_transactional(self, tr, key1, key2): tr.clear_range(key1, key2) # Clears data with random prefixes from a database and a memory key-value store def correctness_clear_prefix(self, db, store, data, num_prefix_clears): prefixes = [] for i in range(0, num_prefix_clears): prefix = "".join( chr(random.randint(0, 254)) for _ in range(0, random.randint(1, 3)) ) prefixes.append(prefix) store.clear_range_startswith(prefix) self.correctness_clear_prefix_transactional(db, prefixes) # Clears keys from a database that have a prefix in the prefixes list @fdb.transactional def correctness_clear_prefix_transactional(self, tr, prefixes): for prefix in prefixes: tr.clear_range_startswith(prefix) # Adds the stack trace to an error message def get_error(self, message): error_message = message + "\n" + traceback.format_exc() print("%s", error_message) return error_message if __name__ == "__main__": print( "Running PythonCorrectness test on Python version %d.%d.%d%s%d" % ( sys.version_info[0], sys.version_info[1], sys.version_info[2], sys.version_info[3][0], sys.version_info[4], ) ) PythonCorrectness().run()