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