diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd092e8c4438a036c90186a330173848cb2bd86..e4288aa79c9d90de1489bff48469e33366c7b7bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features) +* Tests for versioning * Tests for deeply nested SELECT queries * Tests for [#62](https://gitlab.com/caosdb/caosdb-server/-/issues/62) * Tests for One-time Authentication Tokens diff --git a/setup.py b/setup.py index f673cacea5a4b27469fc59719b83bb6124a162d9..d836a2731c2fc1da4d7515917fa21e59b8f3f6b1 100644 --- a/setup.py +++ b/setup.py @@ -3,5 +3,5 @@ setup( name="PyCaosDB Integration Tests", version="0.1.0", packages=find_packages(), - tests_require=["nose>=1.0"], + tests_require=["nose>=1.0", "python-dateutil>=2.8.1"], ) diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index 04ba6f8b2f650c5d2ca6e979050825029e90383e..16e1ec026db33e69ed525752d53dcfc211b1c9a8 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -187,63 +187,47 @@ def test_inheritance_all_properties(): def test_inheritance_unit(): - try: - - p = h.Property( - name="SimpleIntProperty", - datatype="INTEGER", - unit="m").insert() - assert_true(p.is_valid()) - assert_equal("m", p.unit) - - rt = h.RecordType( - name="SimpleRecordType").add_property( - p, unit="km").insert() - assert_true(rt.is_valid()) - assert_equal("km", rt.get_property("SimpleIntProperty").unit) - - rt2 = h.execute_query("FIND SimpleRecordType", True) - assert_true(rt2.is_valid()) - assert_equal(rt2.id, rt.id) - assert_equal(rt2.get_property("SimpleIntProperty").unit, "km") - - rt3 = h.RecordType( - name="SimpleRecordType2").add_parent( - rt, inheritance="ALL").insert() - assert_true(rt3.is_valid()) - assert_is_not_none(rt3.get_property("SimpleIntProperty")) - assert_equal(rt3.get_property("SimpleIntProperty").unit, "km") - - rt4 = h.execute_query("FIND SimpleRecordType2", True) - assert_true(rt4.is_valid()) - assert_equal(rt4.id, rt3.id) - assert_equal(rt4.get_property("SimpleIntProperty").unit, "km") - - rec = h.Record( - name="SimpleRecord").add_parent(rt3).add_property( - name="SimpleIntProperty", - value=1).insert() - assert_true(rec.is_valid()) - assert_is_not_none(rec.get_property("SimpleIntProperty")) - assert_equal(rec.get_property("SimpleIntProperty").unit, "km") - - finally: - try: - rec.delete() - except BaseException: - pass - try: - rt3.delete() - except BaseException: - pass - try: - rt.delete() - except BaseException: - pass - try: - p.delete() - except BaseException: - pass + p = h.Property( + name="SimpleIntProperty", + datatype="INTEGER", + unit="m") + p.insert() + assert p.is_valid() + assert p.unit == "m" + + rt = h.RecordType( + name="SimpleRecordType") + rt.add_property(p, unit="km") + rt.insert() + + assert rt.is_valid() + assert rt.get_property("SimpleIntProperty").unit == "km" + + rt2 = h.execute_query("FIND SimpleRecordType", True) + assert rt2.id == rt.id + assert rt2.get_property("SimpleIntProperty").unit == "km" + + rt3 = h.RecordType( + name="SimpleRecordType2") + rt3.add_parent(rt, inheritance="ALL") + rt3.insert() + assert rt3.is_valid() + assert rt3.get_property("SimpleIntProperty") is not None + assert rt3.get_property("SimpleIntProperty").unit == "km" + + rt4 = h.execute_query("FIND SimpleRecordType2", True) + assert rt4.is_valid() + assert rt4.id == rt3.id + assert rt4.get_property("SimpleIntProperty").unit == "km" + + rec = h.Record( + name="SimpleRecord") + rec.add_parent(rt3) + rec.add_property(name="SimpleIntProperty", value=1) + rec.insert() + assert rec.is_valid() + assert rec.get_property("SimpleIntProperty") is not None + assert rec.get_property("SimpleIntProperty").unit == "km" _ENTITIES = [ diff --git a/tests/test_issues_mysqlbackend.py b/tests/test_issues_mysqlbackend.py index 6b61f0254401ca684feae66b3302898793ef4fe9..214a5c38254c864d5f3677302b8abeab3c0228e3 100644 --- a/tests/test_issues_mysqlbackend.py +++ b/tests/test_issues_mysqlbackend.py @@ -24,9 +24,6 @@ """Tests for issues on gitlab.com, project caosdb-mysqlbackend.""" import caosdb as db -from nose import with_setup -from nose.tools import assert_equal -import pytest def setup_module(): @@ -48,7 +45,6 @@ def teardown(): # ########################### Issue tests start here ##################### -@with_setup(setup, teardown) def test_issue_18(): """Duplicate parents were returned in some cases of self-hineritance. @@ -69,4 +65,4 @@ def test_issue_18(): C1 = db.Entity(name="C").retrieve() pids = [p.id for p in C1.parents] - assert_equal(len(set(pids)), len(pids), "Duplicate parents.") + assert len(set(pids)) == len(pids), "Duplicate parents." diff --git a/tests/test_records.py b/tests/test_records.py index 851090efd01a1daec03a23b1a769af2a7e7777fb..011ebef67568f7b4bd6ff0097e601cef414c5fdb 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -24,11 +24,19 @@ from random import randint from sys import maxsize as maxint from nose.tools import nottest, assert_is_not_none, assert_equal, assert_true, assert_false +from pytest import raises from caosdb import Record from caosdb.exceptions import CaosDBException import caosdb +def teardown(): + try: + caosdb.execute_query("FIND Test*").delete() + except Exception as e: + print(e) + + def test_records(): try: @@ -481,3 +489,16 @@ def test_non_existent(): found = True assert_true(found) + + +def test_insertion_with_ambiguous_parent_name(): + rt1 = caosdb.RecordType("TestParent").insert() + rt2 = caosdb.RecordType("TestParent").insert(unique=False) + + rec = caosdb.Record("TestRec").add_parent( + "TestParent").insert(raise_exception_on_error=False) + assert rec.has_errors() + assert rec.get_errors()[ + "Error", 116][0] == "Entity has unqualified parents." + assert rec.get_parents()[0].get_errors()[ + "Error", 0][0] == "Entity can not be identified due to name duplicates." diff --git a/tests/test_recursive_parents.py b/tests/test_recursive_parents.py index 01bc489b573b7cec187a90245c8ad188b8fd5c92..1efd9390d9a01a417f91bca1cce7f0f76c154e4b 100644 --- a/tests/test_recursive_parents.py +++ b/tests/test_recursive_parents.py @@ -131,14 +131,14 @@ def test_entity_has_parent(): assert not c.has_parent(fake_C_name, check_name=True) fake_B_id = db.RecordType(id=B.id) - fake_C_id = db.RecordType(id=C.id*5) + fake_C_id = db.RecordType(id=C.id * 5) assert c.has_parent(fake_B_id, check_name=False, check_id=True) assert not c.has_parent(fake_C_id, check_name=False, check_id=True) fake_B_name_id = RecordType(name="TestTypeB", id=B.id) - fake_C_name_id = RecordType(name="not C", id=C.id*5) + fake_C_name_id = RecordType(name="not C", id=C.id * 5) assert c.has_parent(fake_B_name_id, check_name=True, check_id=True) diff --git a/tests/test_tickets.py b/tests/test_tickets.py index 8c370cba33624e93220a171a22845f4b2eddfb3e..3afd753beae2673eba0387e657a2292f127097bb 100644 --- a/tests/test_tickets.py +++ b/tests/test_tickets.py @@ -25,33 +25,23 @@ @author: tf """ -from __future__ import absolute_import, print_function, unicode_literals - import caosdb as db from caosdb.exceptions import (AmbiguityException, CaosDBException, EntityDoesNotExistError, EntityError, TransactionError, UniqueNamesError) -from nose import with_setup from nose.tools import (assert_equal, assert_false, assert_is_none, assert_is_not_none, assert_raises, assert_true, nottest) -from tests import test_misc - - -def setup_module(): - try: - db.execute_query("FIND ENTITY WITH ID > 100").delete() - except Exception as e: - print(e) - def setup(): - pass + d = db.execute_query("FIND ENTITY WITH ID > 99") + if len(d) > 0: + d.delete() def teardown(): - setup_module() + setup() def test_ticket_103a(): @@ -294,83 +284,72 @@ def test_ticket_114(): def test_ticket_120(): - - try: - p = db.Property( - name="SimpleDoubleProperty", datatype="DOUBLE").insert() - rt1 = db.RecordType(name="SimpleRT1").insert() - rt2 = db.RecordType(name="SimpleRT2").add_parent(rt1).insert() - rt3 = db.RecordType(name="SimpleRT3").add_parent(rt2).insert() - r1 = db.Record().add_parent(rt1).add_property( - id=p.id, value=3.14).insert() - r2 = db.Record().add_parent(rt2).add_property( - id=rt1.id, value=r1).insert() - r3 = db.Record().add_parent(rt3).add_property( - id=rt2.id, value=r2).insert() - - cp = db.Query("FIND PROPERTY SimpleDoubleProperty").execute( - unique=True) - assert_equal(p.id, cp.id) - - crt123 = db.Query("FIND RECORDTYPE SimpleRT1").execute(unique=False) - assert_true(crt123.get_entity_by_id(rt1.id).is_valid()) - assert_true(crt123.get_entity_by_id(rt2.id).is_valid()) - assert_true(crt123.get_entity_by_id(rt3.id).is_valid()) - - cr1 = db.Query("FIND RECORD . SimpleDoubleProperty='3.14'").execute( - unique=True) - assert_equal(r1.id, cr1.id) - - cr23 = db.Query("FIND RECORD . SimpleRT1").execute(unique=False) - assert_true(cr23.get_entity_by_id(r2.id).is_valid()) - assert_true(cr23.get_entity_by_id(r3.id).is_valid()) - - cr3 = db.Query("FIND RECORD . SimpleRT2").execute(unique=True) - assert_equal(r3.id, cr3.id) - - cr2 = db.Query("FIND RECORD . SimpleRT1->" + str(r1.id)).execute( - unique=True) - assert_equal(r2.id, cr2.id) - - cr3 = db.Query("FIND RECORD . SimpleRT1->" + str(r2.id)).execute( - unique=True) - assert_equal(r3.id, cr3.id) - - cr3 = db.Query( - "FIND RECORD WHICH HAS A PROPERTY blabla=4 OR SimpleRT1->SimpleRT2" - " WHICH HAS A PROPERTY SimpleRT1->" + - str( - r1.id) + - "").execute( - unique=True) - assert_equal(r3.id, cr3.id) - - cr3 = db.Query( - "FIND SimpleRT1 . SimpleRT1.SimpleRT1.SimpleDoubleProperty='3.14'" - ).execute(unique=True) - assert_equal(r3.id, cr3.id) - - cr3 = db.Query( - "FIND RECORD SimpleRT1 . " - "SimpleRT1.SimpleRT1.SimpleDoubleProperty='3.14'" - ).execute(unique=True) - assert_equal(r3.id, cr3.id) - - cr3 = db.Query( - "FIND RECORD . SimpleRT1.SimpleRT1.SimpleDoubleProperty='3.14'" - ).execute(unique=True) - assert_equal(r3.id, cr3.id) - finally: - r3.delete() - r2.delete() - r1.delete() - rt3.delete() - rt2.delete() - rt1.delete() - p.delete() + p = db.Property( + name="SimpleDoubleProperty", datatype="DOUBLE").insert() + rt1 = db.RecordType(name="SimpleRT1").insert() + rt2 = db.RecordType(name="SimpleRT2").add_parent(rt1).insert() + rt3 = db.RecordType(name="SimpleRT3").add_parent(rt2).insert() + r1 = db.Record().add_parent(rt1).add_property( + id=p.id, value=3.14).insert() + r2 = db.Record().add_parent(rt2).add_property( + id=rt1.id, value=r1).insert() + r3 = db.Record().add_parent(rt3).add_property( + id=rt2.id, value=r2).insert() + + cp = db.Query("FIND PROPERTY SimpleDoubleProperty").execute( + unique=True) + assert p.id == cp.id + + crt123 = db.Query("FIND RECORDTYPE SimpleRT1").execute(unique=False) + assert crt123.get_entity_by_id(rt1.id).is_valid() + assert crt123.get_entity_by_id(rt2.id).is_valid() + assert crt123.get_entity_by_id(rt3.id).is_valid() + + cr1 = db.Query("FIND RECORD . SimpleDoubleProperty='3.14'").execute( + unique=True) + assert r1.id == cr1.id + + cr23 = db.Query("FIND RECORD . SimpleRT1").execute(unique=False) + assert cr23.get_entity_by_id(r2.id).is_valid() + assert cr23.get_entity_by_id(r3.id).is_valid() + + cr3 = db.Query("FIND RECORD . SimpleRT2").execute(unique=True) + assert r3.id == cr3.id + + cr2 = db.Query("FIND RECORD . SimpleRT1->" + str(r1.id)).execute( + unique=True) + assert r2.id == cr2.id + + cr3 = db.Query("FIND RECORD . SimpleRT1->" + str(r2.id)).execute( + unique=True) + assert r3.id == cr3.id + + cr3 = db.Query( + "FIND RECORD WHICH HAS A PROPERTY blabla=4 OR SimpleRT1->SimpleRT2" + " WHICH HAS A PROPERTY SimpleRT1->" + + str( + r1.id) + + "").execute( + unique=True) + assert r3.id == cr3.id + + cr3 = db.Query( + "FIND SimpleRT1 . SimpleRT1.SimpleRT1.SimpleDoubleProperty='3.14'" + ).execute(unique=True) + assert r3.id == cr3.id + + cr3 = db.Query( + "FIND RECORD SimpleRT1 . " + "SimpleRT1.SimpleRT1.SimpleDoubleProperty='3.14'" + ).execute(unique=True) + assert r3.id == cr3.id + + cr3 = db.Query( + "FIND RECORD . SimpleRT1.SimpleRT1.SimpleDoubleProperty='3.14'" + ).execute(unique=True) + assert r3.id == cr3.id -@with_setup(setup, teardown) def test_ticket_120a(): p = db.Property(name="TestSimpleDoubleProperty", datatype="DOUBLE") @@ -384,7 +363,6 @@ def test_ticket_120a(): c.delete(raise_exception_on_error=True) -@with_setup(setup, teardown) def test_ticket_117(): p = db.Property( name="TestwaveVelocity", @@ -493,7 +471,6 @@ def test_ticket_86(): f.delete() -@with_setup(setup, teardown) def test_ticket_83(): rt1 = db.RecordType(name="TestRT1").insert() rt2 = db.RecordType(name="TestRT2").insert() @@ -559,7 +536,6 @@ def test_ticket_138(): rt_person.delete() -@with_setup(setup, teardown) def test_ticket_137(): # insert RecordType rt1 = db.RecordType("TestRT1").insert() @@ -580,7 +556,6 @@ def test_ticket_137(): print(e) -@with_setup(setup, teardown) def test_ticket_132(): # insert RecordType rt1 = db.RecordType("TestRT1").insert() @@ -598,7 +573,6 @@ def test_ticket_132(): assert_true(isinstance(p2, db.Property)) -@with_setup(setup, teardown) @nottest def test_ticket_39(): import os diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000000000000000000000000000000000000..40f0cc6bfc3045dd007631bfcf728cbbe093cc5f --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,724 @@ +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +from pytest import mark, raises +from dateutil.parser import parse +import caosdb as c + + +def setup(): + d = c.execute_query("FIND Test*") + if len(d) > 0: + d.delete() + + +def teardown(): + setup() + + +def test_version_object(): + from caosdb.common.versioning import Version + + +def insertion(name="TestRT"): + rt = c.RecordType(name, description="TestDescription1").insert() + assert rt.version is not None + assert rt.version.id is not None + assert rt.version.date is not None + assert len(rt.version.predecessors) == 0 + assert len(rt.version.successors) == 0 + return rt + + +def test_retrieve(): + rt = insertion() + version = rt.version + + rt2 = c.execute_query("FIND RecordType TestRT", unique=True) + assert parse(rt2.version.date) == parse(version.date) + assert rt2.version == version + + +def test_update_description(): + rt = insertion() + old_version = rt.version + old_desc = rt.description + new_desc = "TestDescription2" + rt.description = new_desc + rt.update() + assert rt.description == new_desc + assert rt.version is not None + assert rt.version.id is not None + assert rt.version.date is not None + assert rt.version != old_version + assert rt.version.date != old_version.date + assert parse(rt.version.date) > parse(old_version.date) + + rt2 = c.execute_query("FIND RecordType TestRT", unique=True) + assert rt2.version.id == rt.version.id + assert rt2.version == rt.version + assert rt2.description == new_desc + + rt3 = c.Container().retrieve(query=str(rt.id), sync=False)[0] + assert rt3.version.id == rt.version.id + assert rt3.version == rt.version + assert rt3.description == new_desc + + # retrieve old version + rt_old = c.Container().retrieve(query=str(rt.id) + + "@" + old_version.id, sync=False)[0] + assert rt_old.version.id == old_version.id + assert rt_old.description == old_desc + + +def test_update_parent(): + par1 = insertion("TestRTParent1") + par2 = insertion("TestRTParent2") + rt = insertion("TestRTChild") + + assert len(rt.get_parents()) == 0 + first_version = rt.version + rt.add_parent(par1) + rt.update() + + assert len(rt.get_parents()) == 1 + assert rt.get_parent("TestRTParent1") is not None + second_version = rt.version + + rt.remove_parent(par1) + assert len(rt.get_parents()) == 0 + rt.add_parent(par2) + rt.update() + assert len(rt.get_parents()) == 1 + assert rt.get_parent("TestRTParent1") is None + assert rt.get_parent("TestRTParent2") is not None + third_version = rt.version + + # now retrieve and look again + assert c.execute_query("FIND TestRTParent1", unique=True).id == par1.id + assert len(c.execute_query("FIND TestRTParent2")) == 2 + assert c.execute_query("FIND TestRTChild", unique=True).id == rt.id + + rt_head = c.Container().retrieve(query=str(rt.id), sync=False)[0] + rt_v1 = c.Container().retrieve(query=str(rt.id) + "@" + first_version.id, + sync=False)[0] + rt_v2 = c.Container().retrieve(query=str(rt.id) + "@" + second_version.id, + sync=False)[0] + rt_v3 = c.Container().retrieve(query=str(rt.id) + "@" + third_version.id, + sync=False)[0] + + assert rt_head.version == third_version + assert rt_v1.version.id == first_version.id + assert rt_v2.version.id == second_version.id + assert rt_v3.version.id == third_version.id + + assert len(rt_v3.get_parents()) == 1 + assert rt_v3.get_parent("TestRTParent1") is None + assert rt_v3.get_parent("TestRTParent2") is not None + + assert len(rt_v2.get_parents()) == 1 + assert rt_v2.get_parent("TestRTParent1") is not None + assert rt_v2.get_parent("TestRTParent2") is None + + assert len(rt_v1.get_parents()) == 0 + + +def test_retrieve_old_version(): + rt = insertion() + old_version = rt.version + old_description = rt.description + + rt.description = "TestDescription3" + rt.update() + + rt2 = c.execute_query("FIND RecordType TestRT", unique=True) + assert rt2.description == "TestDescription3" + + rt_old = c.Container().retrieve(query=str(rt.id) + "@" + old_version.id, + sync=False)[0] + assert rt_old.id == rt.id + assert rt_old.description == old_description + + +def test_successor(): + rt = insertion() + old_version = rt.version + + rt.description = "TestDescription5" + rt.update() + + rt2 = c.execute_query("FIND RecordType TestRT", unique=True) + + rt_old = c.Container().retrieve(query=str(rt.id) + "@" + old_version.id, + sync=False)[0] + + assert rt_old.version.successors[0].id == rt2.version.id, ( + "old version has successor after retrieval") + + +def test_predecessor(): + rt = insertion() + old_version = rt.version + old_description = rt.description + + rt.description = "TestDescription6" + rt.update() + + assert rt.version.predecessors[0].id == old_version.id, ( + "latest version has predecessor directly after update") + + rt2 = c.execute_query("FIND RecordType TestRT", unique=True) + + assert rt2.version.predecessors[0].id == old_version.id, ( + "latest version has predecessor after retrieval") + + +def test_retrieve_relative_to_head(): + rt = insertion() + first_version = rt.version + + # retrieve HEAD + rt_head = c.Container().retrieve(query=str(rt.id) + "@HEAD", + sync=False)[0] + rt_head2 = c.Container().retrieve(query=str(rt.id), sync=False)[0] + rt_head2.version = rt_head.version + assert first_version == rt_head.version, "head is first version" + + # no HEAD~1 before first update + with raises(c.EntityDoesNotExistError) as exc: + # no head~2 + c.Container().retrieve(query=str(rt.id) + "@HEAD~1", sync=False) + + # update + rt.description = "TestDescription4" + rt.update() + + new_head_version = rt.version + assert first_version != new_head_version, ( + "first version is not head anymore") + assert new_head_version.predecessors[0] == first_version, ( + "first version is predessor of head") + + # retrieve HEAD (which should have changed after the update) + rt_new_head = c.Container().retrieve(query=str(rt.id) + "@HEAD", + sync=False)[0] + rt_new_head2 = c.Container().retrieve(query=str(rt.id), sync=False)[0] + assert rt_new_head2.version == rt_new_head.version + assert rt_new_head.version == new_head_version, ( + "head is version after update") + assert rt_new_head.version.predecessors[0] == first_version, ( + "predecessor of head is first version (after update)") + + # retrieve HEAD~1 (the pre-update version) + rt_pre_head = c.Container().retrieve(query=str(rt.id) + "@HEAD~1", + sync=False)[0] + assert rt_pre_head.version.id == first_version.id, ( + "head~1 is first version (after update)") + assert rt_pre_head.version.successors[0].id == rt_new_head.version.id, ( + "successor of head~1 is head") + + with raises(c.EntityDoesNotExistError) as exc: + # no head~2 + c.Container().retrieve(query=str(rt.id) + "@HEAD~2", sync=False) + + +@mark.xfail(reason="bug fix needed") +def test_bug_cached_delete(): + rt = insertion() + old_version = rt.version.id + + rt.description = "UpdatedDesc" + rt.update() + + # now id@old_version is cached... + rt2 = c.Container().retrieve(query=str(rt.id) + "@" + old_version, + sync=False)[0] + + c.execute_query("FIND RecordType TestRT").delete() + + with raises(c.EntityDoesNotExistError) as exc: + c.Container().retrieve(query=str(rt.id) + "@" + old_version, + sync=False)[0] + + +@mark.xfail(reason=("TODO: What is the desired behavior? " + "Resolve in versioning phase 10")) +def test_delete_property_used_in_old_version(): + p = c.Property(name="TestProp1", datatype=c.TEXT).insert() + del_p = c.Property(name="TestProp2", datatype=c.TEXT).insert() + rt = c.RecordType(name="TestRT") + rt.add_property(del_p, "blubblub") + rt.insert() + + # can't delete the property used by rt@HEAD + with raises(c.TransactionError) as exc: + del_p.delete() + assert "Entity is required by other entities" in str(exc.value) + + # now update rt and remove the property which is to be deleted + rt.remove_property(del_p) + rt.add_property(p, "blablabla") + rt.update() + + # retrieve and check old version + old_rt = c.Container().retrieve(str(rt.id) + "@HEAD~1", + sync=False)[0] + assert old_rt.get_property(p) is None + assert old_rt.get_property(del_p).value == "blubblub" + + # delete the property use by old_rt + del_p_id = del_p.id + del_p_name = del_p.name + del_p.delete() + + # retrieve old version again + old_rt = c.Container().retrieve(str(rt.id) + "@HEAD~1", + sync=False, + raise_exception_on_error=False)[0] + assert old_rt.get_property(p) is None + + # the value is still there (and the property has nothing but an id) + assert old_rt.get_property(del_p_name) is None + assert old_rt.get_property(del_p_id).value == "blubblub" + + # TODO: What is the desired behavior? + # Currently, the server throws an error. + # Should we make this a warning? + assert "Entity has unqualified properties" in str(old_rt.get_errors()[0]) + + # fails until resolved! + assert len(old_rt.get_errors()) == 0 + + +@mark.xfail(reason=("TODO: What is the desired behavior? " + "Resolve in versioning phase 10")) +def test_delete_parent_used_in_old_version(): + del_rt = c.RecordType(name="TestRT1").insert() + rt = c.RecordType(name="TestRT2").insert() + + rec = c.Record(name="TestRec").add_parent(del_rt) + rec.insert() + + # can't delete the parent used by rec@HEAD + with raises(c.TransactionError) as exc: + del_rt.delete() + assert "Entity is required by other entities" in str(exc.value) + + # update rec and change parent + rec.remove_parent(del_rt) + rec.add_parent(rt) + rec.update() + + # retrieve old version + old_rec = c.Container().retrieve(str(rec.id) + "@HEAD~1", + sync=False)[0] + assert old_rec.get_parent(rt) is None + assert old_rec.get_parent(del_rt) is not None + + del_rt_id = del_rt.id + del_rt_name = del_rt.name + del_rt.delete() + + # retrieve old version, again + old_rec = c.Container().retrieve(str(rec.id) + "@HEAD~1", + sync=False, + raise_exception_on_error=False)[0] + + assert old_rec.get_parent(rt) is None + assert old_rec.get_parent(del_rt_id) is not None + + # TODO: What is the desired behavior? + # Currently, the server doesn't report anything + # Should we issue a warning? + assert len(old_rec.get_messages()) == 0 + + # fail until resolved + assert len(old_rec.get_errors()) > 0 + assert "Entity has unqualified parents" in str(old_rec.get_errors()[0]) + + # Another problem is caching. This fails because the parent name is still + # in the cache. + assert old_rec.get_parent(del_rt_name) is None + + +@mark.xfail(reason="bug fix needed") +def test_bug_cached_parent_name_in_old_version(): + del_rt = c.RecordType(name="TestRT1").insert() + rt = c.RecordType(name="TestRT2").insert() + + rec = c.Record(name="TestRec").add_parent(del_rt) + rec.insert() + + # update rec and change parent + rec.remove_parent(del_rt) + rec.add_parent(rt) + rec.update() + + # delete old parent + del_rt_name = del_rt.name + del_rt.delete() + + # retrieve old version + old_rec = c.Container().retrieve(str(rec.id) + "@HEAD~1", + sync=False, + raise_exception_on_error=False)[0] + + assert old_rec.get_parent(rt) is None + + # This fails because the parent name is still in the cache. + # The name should be forgotten. + assert old_rec.get_parent(del_rt_name) is None + + +def test_reference_deleted_in_old_version(): + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + p = c.Property(name="TestProp", datatype=c.TEXT).insert() + + referenced_rec = c.Record(name="TestRec1").add_parent(ref_rt) + referenced_rec.insert() + + rec = c.Record(name="TestRec2").add_parent(rt) + rec.add_property(p, "blablabla") + rec.add_property(ref_rt, referenced_rec) + rec.insert() + old_version = rec.version.id + + test_rec = c.execute_query( + "FIND RECORD TestRec2 WHICH REFERENCES {}".format(referenced_rec.id), + unique=True) + assert test_rec.get_property(p).value == "blablabla" + assert test_rec.get_property(ref_rt).value == referenced_rec.id + + # deletion of the referenced_rec not possible because rec@HEAD is + # still pointing at it + with raises(c.TransactionError) as exc: + referenced_rec.delete() + assert "Entity is required by other entities" in str(exc.value) + + # update rec + rec.remove_property(ref_rt) + rec.update() + + with raises(c.EntityDoesNotExistError) as exc: + c.execute_query( + "FIND RECORD TestRec2 WHICH REFERENCES {}".format( + referenced_rec.id), + unique=True) + + test_rec = c.execute_query("FIND RECORD WITH TestProp = blablabla", + unique=True) + assert test_rec.get_property(p).value == "blablabla" + assert test_rec.get_property(ref_rt) is None + assert test_rec.version.predecessors[0].id == old_version + + # retrieve old version + old_rec = c.Container().retrieve(str(test_rec.id) + "@HEAD~1", + sync=False)[0] + assert old_rec.version.id == old_version + assert old_rec.version.successors[0].id == test_rec.version.id + assert old_rec.get_property(p).value == "blablabla" + assert old_rec.get_property(ref_rt).value == referenced_rec.id + + # deletion of the referenced_rec now possible because rec@HEAD is not + # pointing at it anymore + referenced_id = referenced_rec.id + referenced_rec.delete() + + # still everything ok + test_rec = c.execute_query("FIND RECORD WITH TestProp = blablabla", + unique=True) + assert test_rec.get_property(p).value == "blablabla" + assert test_rec.get_property(ref_rt) is None + assert test_rec.version.predecessors[0].id == old_version + + # retrieve old version again. the reference (to the now deleted entity) + # is still there. + old_rec = c.Container().retrieve(str(test_rec.id) + "@HEAD~1", + sync=False)[0] + assert old_rec.version.id == old_version + assert old_rec.version.successors[0].id == test_rec.version.id + assert old_rec.get_property(p).value == "blablabla" + assert old_rec.get_property(ref_rt).value == referenced_id + + with raises(c.EntityDoesNotExistError) as exc: + c.execute_query("FIND ENTITY WITH ID = {}".format(referenced_id), + unique=True) + + with raises(c.EntityDoesNotExistError) as exc: + c.Record(id=referenced_id).retrieve() + + +def test_reference_version_head(): + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + + versioned_rec = c.Record(name="TestRec1").add_parent(ref_rt) + versioned_rec.insert() + version = versioned_rec.version.id + rec = c.Record(name="TestRec2").add_parent(rt) + rec.add_property(ref_rt, str(versioned_rec.id) + "@HEAD") + rec = rec.insert(sync=False) + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=version) + + test_rec = c.execute_query( + "FIND TestRec2 WHICH HAS A TestReferencedObject", unique=True) + assert test_rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=version) + + # now update versioned_rec + old_head = versioned_rec.version.id + versioned_rec.description = "new desc" + versioned_rec.update() + + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=old_head), "after update still old head" + + test_rec = c.execute_query( + "FIND TestRec2 WHICH HAS A TestReferencedObject", unique=True) + assert test_rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=old_head), "after query old head" + + +def test_insert_reference_to_head_in_same_container(): + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + + versioned_rec = c.Record(name="TestRec1").add_parent(ref_rt) + versioned_rec.id = -1 + rec = c.Record(name="TestRec2").add_parent(rt) + rec.id = -2 + rec.add_property(ref_rt, str(versioned_rec.id) + "@HEAD") + container = c.Container() + container.extend([versioned_rec, rec]) + container.insert() + + version_id = c.execute_query("FIND Record TestReferencedObject", + unique=True).version.id + test_rec = c.execute_query("FIND Record TestRT WHICH REFERENCES {}".format(versioned_rec.id), + unique=True) + assert test_rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=version_id) + + +def test_update_reference_to_head_minus_one_in_same_container(): + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + + versioned_rec = c.Record(name="TestRec1").add_parent(ref_rt) + versioned_rec.insert() + old_head = versioned_rec.version.id + + rec = c.Record(name="TestRec2").add_parent(rt) + rec.add_property(ref_rt, str(versioned_rec.id)) + rec.insert() + + # now update both + versioned_rec.description = "new description" + + assert rec.get_property(ref_rt).value == versioned_rec.id, "ref to entity" + rec.get_property(ref_rt).value = str(versioned_rec.id) + "@HEAD~1" + + container = c.Container() + container.extend([versioned_rec, rec]) + container.update() + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=versioned_rec.id, ver=old_head), "ref to old_head" + + test_rec = c.execute_query("FIND RECORD TestRT WHICH REFERENCES {}".format(versioned_rec.id), + unique=True) + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=versioned_rec.id, ver=old_head), "after query ref to old_head" + + +def test_update_reference_to_head_minus_one_in_same_container_2(): + """ This is identical to the previous one with one exception: The + referenced entity is not being updated during the transaction but it is + included in the update container. This has caused some buggy behavior in + the past. + """ + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + + versioned_rec = c.Record( + name="TestRec1", + description="v1").add_parent(ref_rt) + versioned_rec.insert() + old_head = versioned_rec.version.id + + # update versioned + versioned_rec.description = "v2" + versioned_rec.update() + + rec = c.Record(name="TestRec2").add_parent(rt) + rec.add_property(ref_rt, str(versioned_rec.id)) + rec.insert() + + # now update only the referencing entity. + assert rec.get_property(ref_rt).value == versioned_rec.id, "ref to entity" + rec.get_property(ref_rt).value = str(versioned_rec.id) + "@HEAD~1" + + container = c.Container() + container.extend([versioned_rec, rec]) + container.update() + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=versioned_rec.id, ver=old_head), "ref to old_head" + + test_rec = c.execute_query("FIND RECORD TestRT WHICH REFERENCES {}".format(versioned_rec.id), + unique=True) + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=versioned_rec.id, ver=old_head), "after query ref to old_head" + + +def test_reference_version_old(): + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + + versioned_rec = c.Record(name="TestRec1").add_parent(ref_rt) + versioned_rec.insert() + version = versioned_rec.version.id + + # now update versioned_rec + old_head = versioned_rec.version.id + versioned_rec.description = "new desc" + versioned_rec.update() + + # insert rec which references an old version of versioned_rec + rec = c.Record(name="TestRec2").add_parent(rt) + rec.add_property(ref_rt, str(versioned_rec.id) + "@" + old_head) + rec = rec.insert(sync=False) + + assert rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=old_head) + + test_rec = c.execute_query( + "FIND TestRec2 WHICH HAS A TestReferencedObject", unique=True) + assert test_rec.get_property(ref_rt).value == "{id}@{ver}".format( + id=str(versioned_rec.id), ver=old_head) + + +def test_reference_no_version(): + ref_rt = insertion("TestReferencedObject") + rt = insertion("TestRT") + + versioned_rec = c.Record(name="TestRec1").add_parent(ref_rt) + versioned_rec.insert() + rec = c.Record(name="TestRec2").add_parent(rt) + rec.add_property(ref_rt, versioned_rec.id) + rec.insert() + assert rec.get_property(ref_rt).value == versioned_rec.id + + test_rec = c.execute_query( + "FIND TestRec2 WHICH HAS A TestReferencedObject", unique=True) + assert test_rec.get_property(ref_rt).value == versioned_rec.id + + +def test_reference_head_minus_in_separate_container(): + ref_rt = insertion("TestRT") + + rec1 = c.Record("TestRecord1-firstVersion").add_parent("TestRT") + rec1.description = "This is the first version." + rec1.insert() + v1 = rec1.version.id + + rec1.name = "TestRecord1-secondVersion" + rec1.description = "This is the second version." + rec1.update() + v2 = rec1.version.id + + rec1.name = "TestRecord1-thirdVersion" + rec1.description = "This is the third version." + rec1.update() + v3 = rec1.version.id + + rec2 = c.Record("TestRecord2").add_parent("TestRT") + rec2.description = ("This record has a list of references to several " + "versions of TestRecord1. The first references the " + "record without specifying the version, the other " + "each reference a different version of that record.") + rec2.add_property("TestRT", datatype=c.LIST("TestRT"), + value=[rec1.id, + str(rec1.id) + "@HEAD", + str(rec1.id) + "@HEAD~1", + str(rec1.id) + "@HEAD~2"]) + rec2.insert() + + test_rec = c.execute_query("FIND TestRecord2", unique=True) + assert test_rec.get_property("TestRT").value == [rec1.id, + str(rec1.id) + "@" + v3, + str(rec1.id) + "@" + v2, + str(rec1.id) + "@" + v1] + + +def test_properties_no_version(): + c.Property("TestProperty", datatype=c.TEXT).insert() + c.RecordType("TestRT").add_property("TestProperty").insert() + + rt = c.execute_query("FIND TestRT", unique=True) + p = rt.get_property("TestProperty") + assert p.version is None + + +def test_update_name(): + old_name = "TestRTOldName" + new_name = "TestRTNewName" + + rt = insertion(old_name) + old_version = rt.version + + assert len(c.execute_query("FIND RecordType {}".format(new_name))) == 0 + rt2 = c.execute_query("FIND RecordType {}".format(old_name), unique=True) + assert rt2.version.id == rt.version.id + assert rt2.version == old_version + assert rt2.name == old_name + + # do the update, run checks again + rt.name = new_name + rt.update() + + assert rt.name == new_name + assert rt.version is not None + assert rt.version.id is not None + assert rt.version.date is not None + assert rt.version != old_version + assert rt.version.date != old_version.date + assert parse(rt.version.date) > parse(old_version.date) + + assert len(c.execute_query("FIND RecordType {}".format(old_name))) == 0 + rt2 = c.execute_query("FIND RecordType {}".format(new_name), unique=True) + assert rt2.version.id == rt.version.id + assert rt2.version == rt.version + assert rt2.name == new_name + + # retrieve once again, via id + rt3 = c.Container().retrieve(query=str(rt.id), sync=False)[0] + assert rt3.version.id == rt.version.id + assert rt3.version == rt.version + assert rt3.name == new_name + + # retrieve old version + rt_old = c.Container().retrieve(query=str(rt.id) + + "@" + old_version.id, sync=False)[0] + assert rt_old.version.id == old_version.id + assert rt_old.name == old_name diff --git a/tox.ini b/tox.ini index 4592bd284267db972fe2f3a6f4520ff125331dd0..babdf47e254755b64599e386c96c708bca2b043c 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,9 @@ setenv = PASSWORD_STORE_DIR = {env:HOME}/.password-store deps=pytest nose pytest-cov + python-dateutil commands_pre=pip install ../caosdb-pylib/ + python --version + python -c "import caosdb; print(caosdb.version.version)" # Add "-x" to stop at first error. commands=pytest --cov=caosdb -vv {posargs}