bash_completion: Enable extglob while parsing file
[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   HOOK_PATH_OK = frozenset([
90     "master-ip-turnup",
91     "master-ip-turndown",
92     ])
93
94   def test(self):
95     """Check whether all hooks are documented.
96
97     """
98     hooksdoc = _ReadDocFile("hooks.rst")
99
100     # Reverse mapping from LU to opcode
101     lu2opcode = dict((lu, op)
102                      for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
103     assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
104       "Found duplicate entries"
105
106     hooks_paths = frozenset(re.findall("^:directory:\s*(.+)\s*$", hooksdoc,
107                                        re.M))
108     self.assertTrue(self.HOOK_PATH_OK.issubset(hooks_paths),
109                     msg="Whitelisted path not found in documentation")
110
111     raw_hooks_ops = re.findall("^OP_(?!CODE$).+$", hooksdoc, re.M)
112     hooks_ops = set()
113     duplicate_ops = set()
114     for op in raw_hooks_ops:
115       if op in hooks_ops:
116         duplicate_ops.add(op)
117       else:
118         hooks_ops.add(op)
119
120     self.assertFalse(duplicate_ops,
121                      msg="Found duplicate opcode documentation: %s" %
122                          utils.CommaJoin(duplicate_ops))
123
124     seen_paths = set()
125     seen_ops = set()
126
127     self.assertFalse(duplicate_ops,
128                      msg="Found duplicated hook documentation: %s" %
129                          utils.CommaJoin(duplicate_ops))
130
131     for name in dir(cmdlib):
132       lucls = getattr(cmdlib, name)
133
134       if (isinstance(lucls, type) and
135           issubclass(lucls, cmdlib.LogicalUnit) and
136           hasattr(lucls, "HPATH")):
137         if lucls.HTYPE is None:
138           continue
139
140         opcls = lu2opcode.get(lucls, None)
141
142         if opcls:
143           seen_ops.add(opcls.OP_ID)
144           self.assertTrue(opcls.OP_ID in hooks_ops,
145                           msg="Missing hook documentation for %s" %
146                               opcls.OP_ID)
147         self.assertTrue(lucls.HPATH in hooks_paths,
148                         msg="Missing documentation for hook %s/%s" %
149                             (lucls.HTYPE, lucls.HPATH))
150         seen_paths.add(lucls.HPATH)
151
152     missed_ops = hooks_ops - seen_ops
153     missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
154
155     self.assertFalse(missed_ops,
156                      msg="Op documents hook not existing anymore: %s" %
157                          utils.CommaJoin(missed_ops))
158
159     self.assertFalse(missed_paths,
160                      msg="Hook path does not exist in opcode: %s" %
161                          utils.CommaJoin(missed_paths))
162
163
164 class TestRapiDocs(unittest.TestCase):
165   def _CheckRapiResource(self, uri, fixup, handler):
166     docline = "%s resource." % uri
167     self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
168                      msg=("First line of %r's docstring is not %r" %
169                           (handler, docline)))
170
171     # Apply fixes before testing
172     for (rx, value) in fixup.items():
173       uri = rx.sub(value, uri)
174
175     self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
176
177   def test(self):
178     """Check whether all RAPI resources are documented.
179
180     """
181     rapidoc = _ReadDocFile("rapi.rst")
182
183     node_name = re.escape("[node_name]")
184     instance_name = re.escape("[instance_name]")
185     group_name = re.escape("[group_name]")
186     job_id = re.escape("[job_id]")
187     disk_index = re.escape("[disk_index]")
188     query_res = re.escape("[resource]")
189
190     resources = connector.GetHandlers(node_name, instance_name, group_name,
191                                       job_id, disk_index, query_res)
192
193     handler_dups = utils.FindDuplicates(resources.values())
194     self.assertFalse(handler_dups,
195                      msg=("Resource handlers used more than once: %r" %
196                           handler_dups))
197
198     uri_check_fixup = {
199       re.compile(node_name): "node1examplecom",
200       re.compile(instance_name): "inst1examplecom",
201       re.compile(group_name): "group4440",
202       re.compile(job_id): "9409",
203       re.compile(disk_index): "123",
204       re.compile(query_res): "lock",
205       }
206
207     assert compat.all(VALID_URI_RE.match(value)
208                       for value in uri_check_fixup.values()), \
209            "Fixup values must be valid URIs, too"
210
211     titles = []
212
213     prevline = None
214     for line in rapidoc.splitlines():
215       if re.match(r"^\++$", line):
216         titles.append(prevline)
217
218       prevline = line
219
220     prefix_exception = frozenset(["/", "/version", "/2"])
221
222     undocumented = []
223     used_uris = []
224
225     for key, handler in resources.iteritems():
226       # Regex objects
227       if hasattr(key, "match"):
228         self.assert_(key.pattern.startswith("^/2/"),
229                      msg="Pattern %r does not start with '^/2/'" % key.pattern)
230         self.assertEqual(key.pattern[-1], "$")
231
232         found = False
233         for title in titles:
234           if title.startswith("``") and title.endswith("``"):
235             uri = title[2:-2]
236             if key.match(uri):
237               self._CheckRapiResource(uri, uri_check_fixup, handler)
238               used_uris.append(uri)
239               found = True
240               break
241
242         if not found:
243           # TODO: Find better way of identifying resource
244           undocumented.append(key.pattern)
245
246       else:
247         self.assert_(key.startswith("/2/") or key in prefix_exception,
248                      msg="Path %r does not start with '/2/'" % key)
249
250         if ("``%s``" % key) in titles:
251           self._CheckRapiResource(key, {}, handler)
252           used_uris.append(key)
253         else:
254           undocumented.append(key)
255
256     self.failIf(undocumented,
257                 msg=("Missing RAPI resource documentation for %s" %
258                      utils.CommaJoin(undocumented)))
259
260     uri_dups = utils.FindDuplicates(used_uris)
261     self.failIf(uri_dups,
262                 msg=("URIs matched by more than one resource: %s" %
263                      utils.CommaJoin(uri_dups)))
264
265     self._FindRapiMissing(resources.values())
266     self._CheckTagHandlers(resources.values())
267
268   def _FindRapiMissing(self, handlers):
269     used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
270                                           handlers)))
271
272     unexpected = used & RAPI_OPCODE_EXCLUDE
273     self.assertFalse(unexpected,
274       msg=("Found RAPI resources for excluded opcodes: %s" %
275            utils.CommaJoin(_GetOpIds(unexpected))))
276
277     missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
278                RAPI_OPCODE_EXCLUDE)
279     self.assertFalse(missing,
280       msg=("Missing RAPI resources for opcodes: %s" %
281            utils.CommaJoin(_GetOpIds(missing))))
282
283   def _CheckTagHandlers(self, handlers):
284     tag_handlers = filter(lambda x: issubclass(x, rlib2._R_Tags), handlers)
285     self.assertEqual(frozenset(map(operator.attrgetter("TAG_LEVEL"),
286                                    tag_handlers)),
287                      constants.VALID_TAG_TYPES)
288
289
290 def _GetOpIds(ops):
291   """Returns C{OP_ID} for all opcodes in passed sequence.
292
293   """
294   return sorted(opcls.OP_ID for opcls in ops)
295
296
297 class TestManpages(unittest.TestCase):
298   """Manpage tests"""
299
300   @staticmethod
301   def _ReadManFile(name):
302     return utils.ReadFile("%s/man/%s.rst" %
303                           (testutils.GetSourceDir(), name))
304
305   @staticmethod
306   def _LoadScript(name):
307     return build.LoadModule("scripts/%s" % name)
308
309   def test(self):
310     for script in _autoconf.GNT_SCRIPTS:
311       self._CheckManpage(script,
312                          self._ReadManFile(script),
313                          self._LoadScript(script).commands.keys())
314
315   def _CheckManpage(self, script, mantext, commands):
316     missing = []
317
318     for cmd in commands:
319       pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
320       if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
321         missing.append(cmd)
322
323     self.failIf(missing,
324                 msg=("Manpage for '%s' missing documentation for %s" %
325                      (script, utils.CommaJoin(missing))))
326
327
328 if __name__ == "__main__":
329   testutils.GanetiTestProgram()