RPC: Add a new client type for DNS only
[ganeti-local] / test / docs_unittest.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2009 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Script for unittesting documentation"""
23
24 import unittest
25 import re
26 import itertools
27 import operator
28
29 from ganeti import _autoconf
30 from ganeti import utils
31 from ganeti import cmdlib
32 from ganeti import build
33 from ganeti import compat
34 from ganeti import mcpu
35 from ganeti import opcodes
36 from ganeti import constants
37 from ganeti.rapi import baserlib
38 from ganeti.rapi import rlib2
39 from ganeti.rapi import connector
40
41 import testutils
42
43
44 VALID_URI_RE = re.compile(r"^[-/a-z0-9]*$")
45
46 RAPI_OPCODE_EXCLUDE = frozenset([
47   # Not yet implemented
48   opcodes.OpBackupQuery,
49   opcodes.OpBackupRemove,
50   opcodes.OpClusterConfigQuery,
51   opcodes.OpClusterRepairDiskSizes,
52   opcodes.OpClusterVerify,
53   opcodes.OpClusterVerifyDisks,
54   opcodes.OpInstanceChangeGroup,
55   opcodes.OpInstanceMove,
56   opcodes.OpNodeQueryvols,
57   opcodes.OpOobCommand,
58   opcodes.OpTagsSearch,
59   opcodes.OpClusterActivateMasterIp,
60   opcodes.OpClusterDeactivateMasterIp,
61
62   # Difficult if not impossible
63   opcodes.OpClusterDestroy,
64   opcodes.OpClusterPostInit,
65   opcodes.OpClusterRename,
66   opcodes.OpNodeAdd,
67   opcodes.OpNodeRemove,
68
69   # Helper opcodes (e.g. submitted by LUs)
70   opcodes.OpClusterVerifyConfig,
71   opcodes.OpClusterVerifyGroup,
72   opcodes.OpGroupEvacuate,
73   opcodes.OpGroupVerifyDisks,
74
75   # Test opcodes
76   opcodes.OpTestAllocator,
77   opcodes.OpTestDelay,
78   opcodes.OpTestDummy,
79   opcodes.OpTestJqueue,
80   ])
81
82
83 def _ReadDocFile(filename):
84   return utils.ReadFile("%s/doc/%s" %
85                         (testutils.GetSourceDir(), filename))
86
87
88 class TestHooksDocs(unittest.TestCase):
89   def test(self):
90     """Check whether all hooks are documented.
91
92     """
93     hooksdoc = _ReadDocFile("hooks.rst")
94
95     # Reverse mapping from LU to opcode
96     lu2opcode = dict((lu, op)
97                      for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
98     assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
99       "Found duplicate entries"
100
101     for name in dir(cmdlib):
102       obj = getattr(cmdlib, name)
103
104       if (isinstance(obj, type) and
105           issubclass(obj, cmdlib.LogicalUnit) and
106           hasattr(obj, "HPATH")):
107         self._CheckHook(name, obj, hooksdoc, lu2opcode)
108
109   def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
110     opcls = lu2opcode.get(lucls, None)
111
112     if lucls.HTYPE is None:
113       return
114
115     # TODO: Improve this test (e.g. find hooks documented but no longer
116     # existing)
117
118     if opcls:
119       self.assertTrue(re.findall("^%s$" % re.escape(opcls.OP_ID),
120                                  hooksdoc, re.M),
121                       msg=("Missing hook documentation for %s" %
122                            (opcls.OP_ID)))
123
124     pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)
125
126     self.assert_(re.findall(pattern, hooksdoc, re.M),
127                  msg=("Missing documentation for hook %s/%s" %
128                       (lucls.HTYPE, lucls.HPATH)))
129
130
131 class TestRapiDocs(unittest.TestCase):
132   def _CheckRapiResource(self, uri, fixup, handler):
133     docline = "%s resource." % uri
134     self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
135                      msg=("First line of %r's docstring is not %r" %
136                           (handler, docline)))
137
138     # Apply fixes before testing
139     for (rx, value) in fixup.items():
140       uri = rx.sub(value, uri)
141
142     self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
143
144   def test(self):
145     """Check whether all RAPI resources are documented.
146
147     """
148     rapidoc = _ReadDocFile("rapi.rst")
149
150     node_name = re.escape("[node_name]")
151     instance_name = re.escape("[instance_name]")
152     group_name = re.escape("[group_name]")
153     job_id = re.escape("[job_id]")
154     disk_index = re.escape("[disk_index]")
155     query_res = re.escape("[resource]")
156
157     resources = connector.GetHandlers(node_name, instance_name, group_name,
158                                       job_id, disk_index, query_res)
159
160     handler_dups = utils.FindDuplicates(resources.values())
161     self.assertFalse(handler_dups,
162                      msg=("Resource handlers used more than once: %r" %
163                           handler_dups))
164
165     uri_check_fixup = {
166       re.compile(node_name): "node1examplecom",
167       re.compile(instance_name): "inst1examplecom",
168       re.compile(group_name): "group4440",
169       re.compile(job_id): "9409",
170       re.compile(disk_index): "123",
171       re.compile(query_res): "lock",
172       }
173
174     assert compat.all(VALID_URI_RE.match(value)
175                       for value in uri_check_fixup.values()), \
176            "Fixup values must be valid URIs, too"
177
178     titles = []
179
180     prevline = None
181     for line in rapidoc.splitlines():
182       if re.match(r"^\++$", line):
183         titles.append(prevline)
184
185       prevline = line
186
187     prefix_exception = frozenset(["/", "/version", "/2"])
188
189     undocumented = []
190     used_uris = []
191
192     for key, handler in resources.iteritems():
193       # Regex objects
194       if hasattr(key, "match"):
195         self.assert_(key.pattern.startswith("^/2/"),
196                      msg="Pattern %r does not start with '^/2/'" % key.pattern)
197         self.assertEqual(key.pattern[-1], "$")
198
199         found = False
200         for title in titles:
201           if title.startswith("``") and title.endswith("``"):
202             uri = title[2:-2]
203             if key.match(uri):
204               self._CheckRapiResource(uri, uri_check_fixup, handler)
205               used_uris.append(uri)
206               found = True
207               break
208
209         if not found:
210           # TODO: Find better way of identifying resource
211           undocumented.append(key.pattern)
212
213       else:
214         self.assert_(key.startswith("/2/") or key in prefix_exception,
215                      msg="Path %r does not start with '/2/'" % key)
216
217         if ("``%s``" % key) in titles:
218           self._CheckRapiResource(key, {}, handler)
219           used_uris.append(key)
220         else:
221           undocumented.append(key)
222
223     self.failIf(undocumented,
224                 msg=("Missing RAPI resource documentation for %s" %
225                      utils.CommaJoin(undocumented)))
226
227     uri_dups = utils.FindDuplicates(used_uris)
228     self.failIf(uri_dups,
229                 msg=("URIs matched by more than one resource: %s" %
230                      utils.CommaJoin(uri_dups)))
231
232     self._FindRapiMissing(resources.values())
233     self._CheckTagHandlers(resources.values())
234
235   def _FindRapiMissing(self, handlers):
236     used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
237                                           handlers)))
238
239     unexpected = used & RAPI_OPCODE_EXCLUDE
240     self.assertFalse(unexpected,
241       msg=("Found RAPI resources for excluded opcodes: %s" %
242            utils.CommaJoin(_GetOpIds(unexpected))))
243
244     missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
245                RAPI_OPCODE_EXCLUDE)
246     self.assertFalse(missing,
247       msg=("Missing RAPI resources for opcodes: %s" %
248            utils.CommaJoin(_GetOpIds(missing))))
249
250   def _CheckTagHandlers(self, handlers):
251     tag_handlers = filter(lambda x: issubclass(x, rlib2._R_Tags), handlers)
252     self.assertEqual(frozenset(map(operator.attrgetter("TAG_LEVEL"),
253                                    tag_handlers)),
254                      constants.VALID_TAG_TYPES)
255
256
257 def _GetOpIds(ops):
258   """Returns C{OP_ID} for all opcodes in passed sequence.
259
260   """
261   return sorted(opcls.OP_ID for opcls in ops)
262
263
264 class TestManpages(unittest.TestCase):
265   """Manpage tests"""
266
267   @staticmethod
268   def _ReadManFile(name):
269     return utils.ReadFile("%s/man/%s.rst" %
270                           (testutils.GetSourceDir(), name))
271
272   @staticmethod
273   def _LoadScript(name):
274     return build.LoadModule("scripts/%s" % name)
275
276   def test(self):
277     for script in _autoconf.GNT_SCRIPTS:
278       self._CheckManpage(script,
279                          self._ReadManFile(script),
280                          self._LoadScript(script).commands.keys())
281
282   def _CheckManpage(self, script, mantext, commands):
283     missing = []
284
285     for cmd in commands:
286       pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
287       if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
288         missing.append(cmd)
289
290     self.failIf(missing,
291                 msg=("Manpage for '%s' missing documentation for %s" %
292                      (script, utils.CommaJoin(missing))))
293
294
295 if __name__ == "__main__":
296   testutils.GanetiTestProgram()