CORD-1337 test cases for client-side ORM

Change-Id: I7344e54610883bb9f23f98b29ec1cda62270b396
diff --git a/xos/xos_client/xosapi/fake_stub.py b/xos/xos_client/xosapi/fake_stub.py
index dc56178..8cecf18 100644
--- a/xos/xos_client/xosapi/fake_stub.py
+++ b/xos/xos_client/xosapi/fake_stub.py
@@ -5,7 +5,6 @@
 
 import functools
 
-ContentTypeIdCounter = 0;
 ContentTypeMap = {}
 
 class FakeObj(object):
@@ -36,6 +35,10 @@
         self.is_set[name] = True
         super(FakeObj, self).__setattr__(name, value)
 
+    @property
+    def self_content_type_id(self):
+        return "xos.%s" % self.__class__.__name__.lower()
+
 class FakeExtensionManager(object):
     def __init__(self, obj, extensions):
         self.obj = obj
@@ -78,8 +81,7 @@
         if objName in ContentTypeMap:
             ct = ContentTypeMap[objName]
         else:
-            ct = ContentTypeIdCounter
-            ContentTypeIdCount = ContentTypeIdCounter + 1
+            ct = "xos.%s" % objName.lower()
             ContentTypeMap[objName] = ct
         self.Extensions = FakeExtensionManager(self, {"xos.contentTypeId": ct})
 
@@ -116,10 +118,41 @@
 
     DESCRIPTOR = FakeDescriptor("Site")
 
+class Service(FakeObj):
+    FIELDS = ( {"name": "id", "default": 0},
+               {"name": "name", "default": ""}, )
+
+    def __init__(self, **kwargs):
+        return super(Service, self).__init__(self.FIELDS, **kwargs)
+
+    DESCRIPTOR = FakeDescriptor("Service")
+
+class Tag(FakeObj):
+    FIELDS = ( {"name": "id", "default": 0},
+               {"name": "service_id", "default": None},
+               {"name": "name", "default": ""},
+               {"name": "value", "default": ""},
+               {"name": "content_type", "default": None},
+               {"name": "object_id", "default": None},
+               {"name": "slice_ids", "default": 0, "fk_reverse": "Slice"} )
+
+    def __init__(self, **kwargs):
+        return super(Tag, self).__init__(self.FIELDS, **kwargs)
+
+    DESCRIPTOR = FakeDescriptor("Tag")
+
+class ID(FakeObj):
+    pass
+
+class FakeItemList(object):
+    def __init__(self, items):
+        self.items = items
+
 class FakeStub(object):
     def __init__(self):
+        self.id_counter = 1
         self.objs = {}
-        for name in ["Slice", "Site"]:
+        for name in ["Slice", "Site", "Tag", "Service"]:
             setattr(self, "Get%s" % name, functools.partial(self.get, name))
             setattr(self, "List%s" % name, functools.partial(self.list, name))
             setattr(self, "Create%s" % name, functools.partial(self.create, name))
@@ -130,35 +163,39 @@
     def make_key(self, name, id):
         return "%s:%d" % (name, id.id)
 
-    def get(self, classname, id):
+    def get(self, classname, id, metadata=None):
         obj = self.objs.get(self.make_key(classname, id), None)
         return obj
 
-    def list(self, classname, empty):
-        items = None
+    def list(self, classname, empty, metadata=None):
+        items = []
         for (k,v) in self.objs.items():
             (this_classname, id) = k.split(":")
             if this_classname == classname:
                     items.append(v)
-        return items
+        return FakeItemList(items)
 
-    def create(self, classname, obj):
+    def create(self, classname, obj, metadata=None):
+        obj.id = self.id_counter
+        self.id_counter = self.id_counter + 1
         k = self.make_key(classname, FakeObj(id=obj.id))
         self.objs[k] = obj
+        return obj
 
-    def update(self, classname, obj):
+    def update(self, classname, obj, metadata=None):
         # TODO: partial update support?
         k = self.make_key(classname, FakeObj(id=obj.id))
         self.objs[k] = obj
+        return obj
 
-    def delete(self, classname, id):
+    def delete(self, classname, id, metadata=None):
         k = self.make_key(classname, id)
         del self.objs[k]
 
 class FakeSymDb(object):
     def __init__(self):
         self._classes = {}
-        for name in ["Slice", "Site"]:
+        for name in ["Slice", "Site", "ID", "Tag", "Service"]:
             self._classes["xos.%s" % name] = globals()[name]
 
 
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index 35f7a3f..d9f656b 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -386,7 +386,7 @@
         return self.objects.new(*args, **kwargs)
 
 class ORMStub(object):
-    def __init__(self, stub, package_name, invoker=None, caller_kind="grpcapi", sym_db = None, empty = None):
+    def __init__(self, stub, package_name, invoker=None, caller_kind="grpcapi", sym_db = None, empty = None, enable_backoff=True):
         self.grpc_stub = stub
         self.all_model_names = []
         self.all_grpc_classes = {}
@@ -394,6 +394,7 @@
         self.reverse_content_type_map = {}
         self.invoker = invoker
         self.caller_kind = caller_kind
+        self.enable_backoff = enable_backoff
 
         if not sym_db:
             from google.protobuf import symbol_database as _symbol_database
@@ -446,9 +447,10 @@
             # Hook in place to call Chameleon's invoke method, as soon as we
             # have rewritten the synchronizer to use reactor.
             return self.invoker.invoke(self.grpc_stub.__class__, name, request, metadata={}).result[0]
-        else:
+        elif self.enable_backoff:
             # Our own retry mechanism. This works fine if there is a temporary
             # failure in connectivity, but does not re-download gRPC schema.
+            import grpc
             while True:
                 backoff = [0.5, 1, 2, 4, 8]
                 try:
@@ -464,6 +466,10 @@
                         raise
                 except:
                     raise
+        else:
+            method = getattr(self.grpc_stub, name)
+            return method(request, metadata=metadata)
+
 
     def make_ID(self, id):
         return self._sym_db._classes["xos.ID"](id=id)
@@ -506,3 +512,4 @@
 import convenience.port
 import convenience.tag
 import convenience.vtrtenant
+
diff --git a/xos/xos_client/xosapi/orm_test.py b/xos/xos_client/xosapi/orm_test.py
index 39d13ad..f276e61 100644
--- a/xos/xos_client/xosapi/orm_test.py
+++ b/xos/xos_client/xosapi/orm_test.py
@@ -16,7 +16,7 @@
     def make_coreapi(self):
         if USE_FAKE_STUB:
             stub = FakeStub()
-            api = ORMStub(stub=stub, package_name = "xos", sym_db = FakeSymDb(), empty = FakeObj)
+            api = xosapi.orm.ORMStub(stub=stub, package_name = "xos", sym_db = FakeSymDb(), empty = FakeObj, enable_backoff = False)
             return api
         else:
             return xos_grpc_client.coreapi
@@ -57,11 +57,110 @@
         self.assertNotEqual(s, None)
         self.assertEqual(s.dumpstr(), '')
 
+    def test_create(self):
+        orm = self.make_coreapi()
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+
+    def test_get(self):
+        orm = self.make_coreapi()
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+        got_site = orm.Site.objects.get(id = site.id)
+        self.assertNotEqual(got_site, None)
+        self.assertEqual(got_site.id, site.id)
+
+    def test_delete(self):
+        orm = self.make_coreapi()
+        orig_len_sites = len(orm.Site.objects.all())
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+        site.delete()
+        sites = orm.Site.objects.all()
+        self.assertEqual(len(sites), orig_len_sites)
+
+    def test_objects_all(self):
+        orm = self.make_coreapi()
+        orig_len_sites = len(orm.Site.objects.all())
+        site = orm.Site(name="mysite")
+        site.save()
+        sites = orm.Site.objects.all()
+        self.assertEqual(len(sites), orig_len_sites+1)
+
+    def test_objects_first(self):
+        orm = self.make_coreapi()
+        site = orm.Site(name="mysite")
+        site.save()
+        site = orm.Site.objects.first()
+        self.assertNotEqual(site, None)
+
+    def test_content_type_map(self):
+        orm = self.make_coreapi()
+        self.assertTrue( "Slice" in orm.content_type_map.values() )
+        self.assertTrue( "Site" in orm.content_type_map.values() )
+        self.assertTrue( "Tag" in orm.content_type_map.values() )
+
+    def test_foreign_key_get(self):
+        orm = self.make_coreapi()
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+        slice = orm.Slice(name="mysite_foo", site_id = site.id)
+        slice.save()
+        self.assertTrue(slice.id > 0)
+        self.assertNotEqual(slice.site, None)
+        self.assertEqual(slice.site.id, site.id)
+
+    def test_foreign_key_set(self):
+        orm = self.make_coreapi()
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+        slice = orm.Slice(name="mysite_foo", site = site)
+        slice.save()
+        slice.invalidate_cache()
+        self.assertTrue(slice.id > 0)
+        self.assertNotEqual(slice.site, None)
+        self.assertEqual(slice.site.id, site.id)
+
+    def test_generic_foreign_key_get(self):
+        orm = self.make_coreapi()
+        service = orm.Service(name="myservice")
+        service.save()
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+        tag = orm.Tag(service=service, name="mytag", value="somevalue", content_type=site.self_content_type_id, object_id=site.id)
+        tag.save()
+        self.assertTrue(tag.id > 0)
+        self.assertNotEqual(tag.content_object, None)
+        self.assertEqual(tag.content_object.id, site.id)
+
+    def test_generic_foreign_key_set(self):
+        orm = self.make_coreapi()
+        service = orm.Service(name="myservice")
+        service.save()
+        site = orm.Site(name="mysite")
+        site.save()
+        self.assertTrue(site.id > 0)
+        tag = orm.Tag(service=service, name="mytag", value="somevalue")
+        tag.content_object = site
+        tag.invalidate_cache()
+        self.assertEqual(tag.content_type, site.self_content_type_id)
+        self.assertEqual(tag.object_id, site.id)
+        tag.save()
+        self.assertTrue(tag.id > 0)
+        self.assertNotEqual(tag.content_object, None)
+        self.assertEqual(tag.content_object.id, site.id)
+
 if USE_FAKE_STUB:
     sys.path.append("..")
 
+    import xosapi.orm
     from fake_stub import FakeStub, FakeSymDb, FakeObj
-    from orm import ORMStub
 
     print "Using Fake Stub"
 
@@ -77,6 +176,7 @@
 
     def test_callback():
         try:
+            sys.argv = sys.argv[:1] # unittest does not like xos_grpc_client's command line arguments (TODO: find a cooperative approach)
             unittest.main()
         except exceptions.SystemExit, e:
             global exitStatus