def test_purge(historian: mincepy.Historian): car = Car() garage = testing.Garage(mincepy.ObjRef(car)) car_id, _ = historian.save(car, garage) # Now update the car car.colour = 'blue' car.save() records_count = historian.records.find().count() # This should not perge anything as v0 of the car has a reference from the garage, while # v1 is the current, live, version res = historian.purge(dry_run=False) assert not res.deleted_purged assert not res.unreferenced_purged assert records_count == historian.records.find().count() # Now mutate the car, save and purge again car.colour = 'white' car.save() # This time, version 1 is unreferenced by anything and so can be safely deleted res = historian.purge(dry_run=False) assert not res.deleted_purged assert res.unreferenced_purged == {mincepy.SnapshotId(car_id, 1)} assert records_count == historian.records.find().count()
def test_storing_internal_object(historian: mincepy.Historian): class Person(mincepy.SavableObject): TYPE_ID = uuid.UUID('f6f83595-6375-4bc4-89f2-d8f31a1286b0') def __init__(self, car): super().__init__() self.car = car # This person 'owns' the car def __eq__(self, other): return self.car == other.car def yield_hashables(self, hasher): yield from hasher.yield_hashables(self.car) def save_instance_state(self, _depositor): return {'car': self.car} def load_instance_state(self, saved_state, _depositor): self.car = saved_state['car'] ferrari = Car('ferrari') mike = Person(ferrari) mike_id = historian.save(mike) del mike loaded_mike = historian.load(mike_id) assert loaded_mike.car.make == 'ferrari' # Default is to save by value so two cars should not be the same assert loaded_mike.car is not ferrari assert loaded_mike.car == ferrari # But values should match
def test_ref_dict(historian: mincepy.Historian): """Test that the ref dict correctly stores entries as references""" dict1 = mincepy.builtins.RefDict() dict2 = mincepy.builtins.RefDict() car = Car() dict1['car'] = car dict2['car'] = car # Test iteration for key, value in dict1.items(): assert key == 'car' assert value is car # Reference condition satisfied assert dict1['car'] is dict2['car'] # Now delete everything, reload, and make sure the condition is still satisfied dict1_id, dict2_id = historian.save(dict1, dict2) del dict1, dict2, car dict1_loaded = historian.load(dict1_id) dict2_loaded = historian.load(dict2_id) assert len(dict1_loaded) == 1 assert len(dict2_loaded) == 1 assert isinstance(dict1_loaded['car'], Car) assert dict1_loaded['car'] is dict2_loaded['car']
def test_merge_file(historian: mincepy.Historian): """Test that merging files works correctly""" file = historian.create_file('test.dat') file.write_text('bla bla') file.save() with testing.temporary_historian( testing.create_archive_uri(db_name='test_historian')) as remote: local = historian # Merge the file into the remote result = remote.merge(local.find(obj_id=file.obj_id)) assert len(result.all) == len(result.merged) == local.find().count() assert local.find().count() == remote.find().count() remote_file = remote.get(file.obj_id) assert file.read_text() == remote_file.read_text() # Now check that files contained within objects are correctly merged file_list = mincepy.List((file, )) file_list.save() # pylint: disable=no-member result = remote.merge(local.find(obj_id=file_list.obj_id)) # pylint: disable=no-member assert len(result.merged) == 1 assert historian.get_snapshot_id(file_list) in result.merged remote_file_list = remote.get(file_list.obj_id) # pylint: disable=no-member assert file.read_text() == remote_file_list[0].read_text()
def test_lazy_migrating_with_saved(historian: mincepy.Historian): """Test migrating an object that has saved references""" class V3(mincepy.ConvenientSavable): TYPE_ID = uuid.UUID('40377bfc-901c-48bb-a85c-1dd692cddcae') ref = mincepy.field(ref=True) description = mincepy.field() class Migration(mincepy.ObjectMigration): VERSION = 2 PREVIOUS = StoreByRef.ToRefMigration @classmethod def upgrade(cls, saved_state, loader: 'mincepy.Loader'): saved_state['description'] = None LATEST_MIGRATION = Migration def __init__(self, ref): super().__init__() self.ref = ref self.description = None obj = StoreByRef(testing.Car()) obj_id = obj.save() del obj gc.collect() historian.register_type(V3) obj = historian.load(obj_id) assert isinstance(obj, V3) assert hasattr(obj, 'description') assert obj.description is None
def test_concurrent_modification(historian: mincepy.Historian, archive_uri: str): # Create a second historian connected to the same archive historian2 = mincepy.connect(archive_uri, use_globally=False) historian2.register_type(Car) ferrari = testing.Car(colour='red', make='ferrari') ferrari_id = historian.save(ferrari) ferrari2 = historian2.load(ferrari_id) assert ferrari_id == ferrari2.obj_id assert ferrari is not ferrari2, \ "The archive don't know about each other so the objects instances should not be the same" # Repaint ferrari.colour = 'yellow' historian.save(ferrari) # Now change ferrari2 and see what happens ferrari2.colour = 'green' with pytest.raises(mincepy.ModificationError): historian2.save(ferrari2) # Now, let's sync up assert historian2.sync(ferrari2), "ferrari2 hasn't been updated" assert ferrari2.colour == 'yellow'
def test_find_migratable(historian: mincepy.Historian): car = CarV1('white', 'lada') car_id = car.save() by_val = StoreByValue(car) by_val_id = by_val.save() # Register a new version of the car historian.register_type(CarV2) # Now both car and by_val should need migration (because by_val stores a car) migratable = tuple(historian.migrations.find_migratable_records()) assert len(migratable) == 2 ids = [record.obj_id for record in migratable] assert car_id in ids assert by_val_id in ids # Now register a new version of StoreByVal historian.register_type(StoreByRef) # There should still be the same to migratables as before migratable = tuple(historian.migrations.find_migratable_records()) assert len(migratable) == 2 ids = [record.obj_id for record in migratable] assert car_id in ids assert by_val_id in ids
def test_encode_nested(historian: mincepy.Historian): class CarDelegate(mincepy.SimpleSavable): TYPE_ID = uuid.UUID('c0148a43-c0c0-4d2b-9262-ed1c8c6ab2fc') ATTRS = ('car',) def __init__(self, car): super().__init__() self.car = car def save_instance_state(self, saver): return {'car': self.car} def load_instance_state(self, saved_state, _loader): self.car = saved_state['car'] historian.register_type(CarDelegate) car = Car() delegate = CarDelegate(car) garage = Garage(delegate) garage_id = historian.save(garage) del garage historian.load(garage_id)
def test_load_changed_ref(historian: mincepy.Historian, archive_uri): """Test what happens when you dereference a reference to an object that was mutated since the reference was loaded""" historian2 = mincepy.connect(archive_uri) car = testing.Car(make='skoda') car_id = historian.save(car) car_ref = mincepy.ref(car) ref_id = historian.save(car_ref) ref_sid = historian.get_snapshot_id(car_ref) del car, car_ref # Now, load the reference but don't dereference it yet loaded = historian.load(ref_id) # Now, mutate the car that is being referenced loaded_car = historian2.load(car_id) loaded_car.make = 'honda' historian2.save(loaded_car) # Finally, dereference and check that it is as expected assert loaded().make == 'honda' # Now, check the snapshot still points to the original loaded_snapshot = historian.load_snapshot(ref_sid) assert loaded_snapshot().make == 'skoda'
def test_nested_files_in_list_mutating(tmp_path, historian: mincepy.Historian): # pylint: disable=unused-argument encoding = 'utf-8' INITIAL_DATA = 'First string'.encode(encoding) my_file = historian.create_file() with my_file.open('wb') as file: file.write(INITIAL_DATA) my_list = mincepy.builtins.List() my_list.append(my_file) list_id = historian.save(my_list) # Now let's append to the file NEW_DATA = 'Second string'.encode(encoding) with my_file.open('ab') as file: file.write(NEW_DATA) # Save the list again historian.save(my_list) del my_list loaded = historian.load(list_id) with loaded[0].open('rb') as contents: buffer = io.BytesIO() shutil.copyfileobj(contents, buffer) assert buffer.getvalue() == INITIAL_DATA + NEW_DATA
def test_ref_list(historian: mincepy.Historian): """Test references list""" list1 = mincepy.RefList() list2 = mincepy.RefList() car = Car() list1.append(car) list2.append(car) # Test iteration for entry in list1: assert entry is car # Reference condition satisfied assert list1[0] is list2[0] # Now delete everything, reload, and make sure the condition is still satisfied list1_id, list2_id = historian.save(list1, list2) assert car.is_saved( ), "The container should automatically save all it's entries saved" del list1, list2, car list1_loaded = historian.load(list1_id) list2_loaded = historian.load(list2_id) assert len(list1_loaded) == 1 assert len(list2_loaded) == 1 assert isinstance(list1_loaded[0], Car) assert list1_loaded[0] is list2_loaded[0]
def test_meta_on_delete(historian: mincepy.Historian): """Test that metadata gets deleted when the object does""" car = Car() car_id = car.save(meta={'reg': '1234'}) assert historian.meta.get(car_id) == {'reg': '1234'} historian.delete(car_id) assert historian.meta.get(car_id) is None
def test_create_delete_load(historian: mincepy.Historian): car = Car('honda', 'red') car_id = historian.save(car) del car loaded_car = historian.load(car_id) assert loaded_car.make == 'honda' assert loaded_car.colour == 'red'
def test_metadata_find_objects(historian: mincepy.Historian): honda = Car('honda', 'white') honda2 = Car('honda', 'white') historian.save_one(honda, meta={'reg': 'H123', 'vin': 1234}) historian.save(honda2) results = list(historian.find(Car, meta={'reg': 'H123'})) assert len(results) == 1
def test_metadata_multiple(historian: mincepy.Historian): honda = Car('honda', 'white') zonda = Car('zonda', 'yellow') historian.save((honda, {'reg': 'H123'}), (zonda, {'reg': 'Z456'})) assert historian.meta.get(honda) == {'reg': 'H123'} assert historian.meta.get(zonda) == {'reg': 'Z456'}
def test_obj_ref_simple(historian: mincepy.Historian): a = testing.Cycle() a.ref = mincepy.ObjRef(a) aid = historian.save(a) del a loaded = historian.load(aid) assert loaded.ref() is loaded
def test_delete_multiple_versions(historian: mincepy.Historian): car = testing.Car('skoda', 'green') car.save() car.colour = 'red' car.save() with historian.transaction(): historian.delete(car)
def test_find_many_cars(historian: mincepy.Historian, benchmark, num): """Test finding a car as a function of the number of entries in the database""" # Put in the correct number of random other entries for _ in range(num): historian.save(Car(utils.random_str(10), utils.random_str(5))) result = benchmark(find, historian) assert len(result) == num
def test_get_snapshot_graph_simple(historian: mincepy.Historian): car = Car() garage = Garage(mincepy.ObjRef(car)) garage.save() garage_sid = historian.get_snapshot_id(garage) garage_graph = historian.archive.get_snapshot_ref_graph(garage_sid) assert len(garage_graph.edges) == 1 assert (garage_sid, historian.get_snapshot_id(car)) in garage_graph.edges
def test_basic_save_load(historian: mincepy.Historian): car = Car('nissan', 'white') car_id = historian.save(car) del car loaded_car = historian.load(car_id) assert loaded_car.make == 'nissan' assert loaded_car.colour == 'white'
def test_cyclic_ref_simple(historian: mincepy.Historian): a = Cycle() a.ref = a # Cycle complete a_id = historian.save(a) del a gc.collect() # Force a garbage collection loaded_a = historian.load(a_id) assert loaded_a.ref is loaded_a
def test_get_snapshot_self_cycle(historian: mincepy.Historian): node = Cycle() node.ref = node # Cycle complete historian.save_one(node) node_ref = historian.get_snapshot_id(node) node_graph = historian.archive.get_snapshot_ref_graph(node_ref) assert len(node_graph.edges) == 1 assert (node_ref, node_ref) in node_graph.edges
def test_loading_snapshot_cycle(historian: mincepy.Historian): a = Cycle() a.ref = a # Close the cycle historian.save(a) a_sid = historian.get_snapshot_id(a) del a loaded = historian.load_snapshot(a_sid) assert loaded.ref is loaded
def test_saving_numpy_arrays(historian: mincepy.Historian): array = numpy.ones(10) array_id = historian.save(array) del array loaded_array = historian.load(array_id) assert all(loaded_array == numpy.ones(10)) loaded_array[0] = 5. historian.save(loaded_array)
def test_delete_from_obj_id(historian: mincepy.Historian): """Test deleting an object using it's object id""" car = testing.Car('skoda') car_id = car.save() del car historian.delete(car_id) with pytest.raises(mincepy.NotFound): historian.load(car_id)
def test_null_ref(historian: mincepy.Historian): null = mincepy.ObjRef() null2 = mincepy.ObjRef() assert null == null2 nid1, _nid2 = historian.save(null, null2) del null loaded = historian.load(nid1) assert loaded == null2
def test_copy_unsaved(historian: mincepy.Historian): car = Car('porsche', 'silver') car_copy = mincepy.copy(car) assert car_copy is not car assert car == car_copy # The cars should not be saved assert not historian.is_known(car) assert not historian.is_known(car_copy)
def test_simple_migration(historian: mincepy.Historian): car = CarV0('red', 'ferrari') car_id = historian.save(car) del car # Now change to version 2 historian.register_type(CarV1) loaded_car = historian.load(car_id) assert loaded_car.colour == 'red' assert loaded_car.make == 'ferrari'
def test_tuple_helper(historian: mincepy.Historian): historian.register_type(mincepy.common_helpers.TupleHelper()) container = mincepy.builtins.List() container.append((Car('ferrari'), )) container_id = historian.save(container) del container loaded = historian.load(container_id) assert loaded[0][0] == Car('ferrari')
def test_find_arg_types(historian: mincepy.Historian): """Test the argument types accepted by the historian find() method""" red_ferrari = testing.Car(colour='red', make='ferrari') green_ferrari = testing.Car(colour='green', make='ferrari') red_honda = testing.Car(colour='red', make='honda') martin = testing.Person(name='martin', age=35, car=red_honda) red_ferrari_id, green_ferrari_id, red_honda_id = \ historian.save(red_ferrari, green_ferrari, red_honda) martin_id = martin.save() # Test different possibilities for object ids being passed list(historian.find(obj_id=red_ferrari_id)) list( historian.find( obj_id=[red_ferrari_id, green_ferrari_id, martin_id, red_honda_id ])) list( historian.find(obj_id=(red_ferrari_id, green_ferrari_id, martin_id, red_honda_id))) list(historian.find(obj_id=str(red_ferrari_id))) # Test object types list(historian.find(obj_type=testing.Person)) list(historian.find(obj_type=[testing.Person, testing.Car])) list(historian.find(obj_type=(testing.Person, testing.Car))) list(historian.find(obj_type=testing.Person.TYPE_ID)) list( historian.find(obj_type=[testing.Person.TYPE_ID, testing.Car.TYPE_ID]))