Merge branch 'devel-2.5'
[ganeti-local] / test / docs_unittest.py
index a2a33b8..0db111f 100755 (executable)
 
 import unittest
 import re
+import itertools
+import operator
 
 from ganeti import _autoconf
 from ganeti import utils
 from ganeti import cmdlib
 from ganeti import build
+from ganeti import compat
+from ganeti import mcpu
+from ganeti import opcodes
+from ganeti import constants
+from ganeti.rapi import baserlib
+from ganeti.rapi import rlib2
 from ganeti.rapi import connector
 
 import testutils
 
 
-class TestDocs(unittest.TestCase):
-  """Documentation tests"""
-
-  @staticmethod
-  def _ReadDocFile(filename):
-    return utils.ReadFile("%s/doc/%s" %
-                          (testutils.GetSourceDir(), filename))
-
-  def testHookDocs(self):
+VALID_URI_RE = re.compile(r"^[-/a-z0-9]*$")
+
+RAPI_OPCODE_EXCLUDE = frozenset([
+  # Not yet implemented
+  opcodes.OpBackupQuery,
+  opcodes.OpBackupRemove,
+  opcodes.OpClusterConfigQuery,
+  opcodes.OpClusterRepairDiskSizes,
+  opcodes.OpClusterVerify,
+  opcodes.OpClusterVerifyDisks,
+  opcodes.OpInstanceChangeGroup,
+  opcodes.OpInstanceMove,
+  opcodes.OpNodeQueryvols,
+  opcodes.OpOobCommand,
+  opcodes.OpTagsSearch,
+
+  # Difficult if not impossible
+  opcodes.OpClusterDestroy,
+  opcodes.OpClusterPostInit,
+  opcodes.OpClusterRename,
+  opcodes.OpNodeAdd,
+  opcodes.OpNodeRemove,
+
+  # Helper opcodes (e.g. submitted by LUs)
+  opcodes.OpClusterVerifyConfig,
+  opcodes.OpClusterVerifyGroup,
+  opcodes.OpGroupEvacuate,
+  opcodes.OpGroupVerifyDisks,
+
+  # Test opcodes
+  opcodes.OpTestAllocator,
+  opcodes.OpTestDelay,
+  opcodes.OpTestDummy,
+  opcodes.OpTestJqueue,
+  ])
+
+
+def _ReadDocFile(filename):
+  return utils.ReadFile("%s/doc/%s" %
+                        (testutils.GetSourceDir(), filename))
+
+
+class TestHooksDocs(unittest.TestCase):
+  def test(self):
     """Check whether all hooks are documented.
 
     """
-    hooksdoc = self._ReadDocFile("hooks.rst")
+    hooksdoc = _ReadDocFile("hooks.rst")
+
+    # Reverse mapping from LU to opcode
+    lu2opcode = dict((lu, op)
+                     for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
+    assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
+      "Found duplicate entries"
 
     for name in dir(cmdlib):
       obj = getattr(cmdlib, name)
@@ -53,15 +102,23 @@ class TestDocs(unittest.TestCase):
       if (isinstance(obj, type) and
           issubclass(obj, cmdlib.LogicalUnit) and
           hasattr(obj, "HPATH")):
-        self._CheckHook(name, obj, hooksdoc)
+        self._CheckHook(name, obj, hooksdoc, lu2opcode)
+
+  def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
+    opcls = lu2opcode.get(lucls, None)
 
-  def _CheckHook(self, name, lucls, hooksdoc):
     if lucls.HTYPE is None:
       return
 
     # TODO: Improve this test (e.g. find hooks documented but no longer
     # existing)
 
+    if opcls:
+      self.assertTrue(re.findall("^%s$" % re.escape(opcls.OP_ID),
+                                 hooksdoc, re.M),
+                      msg=("Missing hook documentation for %s" %
+                           (opcls.OP_ID)))
+
     pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)
 
     self.assert_(re.findall(pattern, hooksdoc, re.M),
@@ -69,21 +126,52 @@ class TestDocs(unittest.TestCase):
                       (lucls.HTYPE, lucls.HPATH)))
 
 
-  def testRapiDocs(self):
-    """Check whether all RAPI resources are documented.
+class TestRapiDocs(unittest.TestCase):
+  def _CheckRapiResource(self, uri, fixup, handler):
+    docline = "%s resource." % uri
+    self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
+                     msg=("First line of %r's docstring is not %r" %
+                          (handler, docline)))
 
-    """
-    rapidoc = self._ReadDocFile("rapi.rst")
+    # Apply fixes before testing
+    for (rx, value) in fixup.items():
+      uri = rx.sub(value, uri)
 
-    node_name = "[node_name]"
-    instance_name = "[instance_name]"
-    group_name = "[group_name]"
-    job_id = "[job_id]"
+    self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
+
+  def test(self):
+    """Check whether all RAPI resources are documented.
 
-    resources = connector.GetHandlers(re.escape(node_name),
-                                      re.escape(instance_name),
-                                      re.escape(group_name),
-                                      re.escape(job_id))
+    """
+    rapidoc = _ReadDocFile("rapi.rst")
+
+    node_name = re.escape("[node_name]")
+    instance_name = re.escape("[instance_name]")
+    group_name = re.escape("[group_name]")
+    job_id = re.escape("[job_id]")
+    disk_index = re.escape("[disk_index]")
+    query_res = re.escape("[resource]")
+
+    resources = connector.GetHandlers(node_name, instance_name, group_name,
+                                      job_id, disk_index, query_res)
+
+    handler_dups = utils.FindDuplicates(resources.values())
+    self.assertFalse(handler_dups,
+                     msg=("Resource handlers used more than once: %r" %
+                          handler_dups))
+
+    uri_check_fixup = {
+      re.compile(node_name): "node1examplecom",
+      re.compile(instance_name): "inst1examplecom",
+      re.compile(group_name): "group4440",
+      re.compile(job_id): "9409",
+      re.compile(disk_index): "123",
+      re.compile(query_res): "lock",
+      }
+
+    assert compat.all(VALID_URI_RE.match(value)
+                      for value in uri_check_fixup.values()), \
+           "Fixup values must be valid URIs, too"
 
     titles = []
 
@@ -97,20 +185,24 @@ class TestDocs(unittest.TestCase):
     prefix_exception = frozenset(["/", "/version", "/2"])
 
     undocumented = []
+    used_uris = []
 
     for key, handler in resources.iteritems():
       # Regex objects
       if hasattr(key, "match"):
         self.assert_(key.pattern.startswith("^/2/"),
                      msg="Pattern %r does not start with '^/2/'" % key.pattern)
+        self.assertEqual(key.pattern[-1], "$")
 
         found = False
         for title in titles:
-          if (title.startswith("``") and
-              title.endswith("``") and
-              key.match(title[2:-2])):
-            found = True
-            break
+          if title.startswith("``") and title.endswith("``"):
+            uri = title[2:-2]
+            if key.match(uri):
+              self._CheckRapiResource(uri, uri_check_fixup, handler)
+              used_uris.append(uri)
+              found = True
+              break
 
         if not found:
           # TODO: Find better way of identifying resource
@@ -120,13 +212,52 @@ class TestDocs(unittest.TestCase):
         self.assert_(key.startswith("/2/") or key in prefix_exception,
                      msg="Path %r does not start with '/2/'" % key)
 
-        if ("``%s``" % key) not in titles:
+        if ("``%s``" % key) in titles:
+          self._CheckRapiResource(key, {}, handler)
+          used_uris.append(key)
+        else:
           undocumented.append(key)
 
     self.failIf(undocumented,
                 msg=("Missing RAPI resource documentation for %s" %
                      utils.CommaJoin(undocumented)))
 
+    uri_dups = utils.FindDuplicates(used_uris)
+    self.failIf(uri_dups,
+                msg=("URIs matched by more than one resource: %s" %
+                     utils.CommaJoin(uri_dups)))
+
+    self._FindRapiMissing(resources.values())
+    self._CheckTagHandlers(resources.values())
+
+  def _FindRapiMissing(self, handlers):
+    used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
+                                          handlers)))
+
+    unexpected = used & RAPI_OPCODE_EXCLUDE
+    self.assertFalse(unexpected,
+      msg=("Found RAPI resources for excluded opcodes: %s" %
+           utils.CommaJoin(_GetOpIds(unexpected))))
+
+    missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
+               RAPI_OPCODE_EXCLUDE)
+    self.assertFalse(missing,
+      msg=("Missing RAPI resources for opcodes: %s" %
+           utils.CommaJoin(_GetOpIds(missing))))
+
+  def _CheckTagHandlers(self, handlers):
+    tag_handlers = filter(lambda x: issubclass(x, rlib2._R_Tags), handlers)
+    self.assertEqual(frozenset(map(operator.attrgetter("TAG_LEVEL"),
+                                   tag_handlers)),
+                     constants.VALID_TAG_TYPES)
+
+
+def _GetOpIds(ops):
+  """Returns C{OP_ID} for all opcodes in passed sequence.
+
+  """
+  return sorted(opcls.OP_ID for opcls in ops)
+
 
 class TestManpages(unittest.TestCase):
   """Manpage tests"""