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