4 # Copyright (C) 2009 Google Inc.
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.
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.
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
22 """Script for unittesting documentation"""
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
44 VALID_URI_RE = re.compile(r"^[-/a-z0-9]*$")
46 RAPI_OPCODE_EXCLUDE = frozenset([
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,
59 opcodes.OpClusterActivateMasterIp,
60 opcodes.OpClusterDeactivateMasterIp,
62 # Difficult if not impossible
63 opcodes.OpClusterDestroy,
64 opcodes.OpClusterPostInit,
65 opcodes.OpClusterRename,
69 # Very sensitive in nature
70 opcodes.OpRestrictedCommand,
72 # Helper opcodes (e.g. submitted by LUs)
73 opcodes.OpClusterVerifyConfig,
74 opcodes.OpClusterVerifyGroup,
75 opcodes.OpGroupEvacuate,
76 opcodes.OpGroupVerifyDisks,
79 opcodes.OpTestAllocator,
86 def _ReadDocFile(filename):
87 return utils.ReadFile("%s/doc/%s" %
88 (testutils.GetSourceDir(), filename))
91 class TestHooksDocs(unittest.TestCase):
92 HOOK_PATH_OK = frozenset([
98 """Check whether all hooks are documented.
101 hooksdoc = _ReadDocFile("hooks.rst")
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"
109 hooks_paths = frozenset(re.findall("^:directory:\s*(.+)\s*$", hooksdoc,
111 self.assertTrue(self.HOOK_PATH_OK.issubset(hooks_paths),
112 msg="Whitelisted path not found in documentation")
114 raw_hooks_ops = re.findall("^OP_(?!CODE$).+$", hooksdoc, re.M)
116 duplicate_ops = set()
117 for op in raw_hooks_ops:
119 duplicate_ops.add(op)
123 self.assertFalse(duplicate_ops,
124 msg="Found duplicate opcode documentation: %s" %
125 utils.CommaJoin(duplicate_ops))
130 self.assertFalse(duplicate_ops,
131 msg="Found duplicated hook documentation: %s" %
132 utils.CommaJoin(duplicate_ops))
134 for name in dir(cmdlib):
135 lucls = getattr(cmdlib, name)
137 if (isinstance(lucls, type) and
138 issubclass(lucls, cmdlib.LogicalUnit) and
139 hasattr(lucls, "HPATH")):
140 if lucls.HTYPE is None:
143 opcls = lu2opcode.get(lucls, None)
146 seen_ops.add(opcls.OP_ID)
147 self.assertTrue(opcls.OP_ID in hooks_ops,
148 msg="Missing hook documentation for %s" %
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)
155 missed_ops = hooks_ops - seen_ops
156 missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
158 self.assertFalse(missed_ops,
159 msg="Op documents hook not existing anymore: %s" %
160 utils.CommaJoin(missed_ops))
162 self.assertFalse(missed_paths,
163 msg="Hook path does not exist in opcode: %s" %
164 utils.CommaJoin(missed_paths))
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" %
174 # Apply fixes before testing
175 for (rx, value) in fixup.items():
176 uri = rx.sub(value, uri)
178 self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
181 """Check whether all RAPI resources are documented.
184 rapidoc = _ReadDocFile("rapi.rst")
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]")
194 resources = connector.GetHandlers(node_name, instance_name,
195 group_name, network_name,
196 job_id, disk_index, query_res)
198 handler_dups = utils.FindDuplicates(resources.values())
199 self.assertFalse(handler_dups,
200 msg=("Resource handlers used more than once: %r" %
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",
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"
220 for line in rapidoc.splitlines():
221 if re.match(r"^\++$", line):
222 titles.append(prevline)
226 prefix_exception = frozenset(["/", "/version", "/2"])
231 for key, handler in resources.iteritems():
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], "$")
240 if title.startswith("``") and title.endswith("``"):
243 self._CheckRapiResource(uri, uri_check_fixup, handler)
244 used_uris.append(uri)
249 # TODO: Find better way of identifying resource
250 undocumented.append(key.pattern)
253 self.assert_(key.startswith("/2/") or key in prefix_exception,
254 msg="Path %r does not start with '/2/'" % key)
256 if ("``%s``" % key) in titles:
257 self._CheckRapiResource(key, {}, handler)
258 used_uris.append(key)
260 undocumented.append(key)
262 self.failIf(undocumented,
263 msg=("Missing RAPI resource documentation for %s" %
264 utils.CommaJoin(undocumented)))
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)))
271 self._FindRapiMissing(resources.values())
272 self._CheckTagHandlers(resources.values())
274 def _FindRapiMissing(self, handlers):
275 used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
278 unexpected = used & RAPI_OPCODE_EXCLUDE
279 self.assertFalse(unexpected,
280 msg=("Found RAPI resources for excluded opcodes: %s" %
281 utils.CommaJoin(_GetOpIds(unexpected))))
283 missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
285 self.assertFalse(missing,
286 msg=("Missing RAPI resources for opcodes: %s" %
287 utils.CommaJoin(_GetOpIds(missing))))
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"),
293 constants.VALID_TAG_TYPES)
297 """Returns C{OP_ID} for all opcodes in passed sequence.
300 return sorted(opcls.OP_ID for opcls in ops)
303 class TestManpages(unittest.TestCase):
307 def _ReadManFile(name):
308 return utils.ReadFile("%s/man/%s.rst" %
309 (testutils.GetSourceDir(), name))
312 def _LoadScript(name):
313 return build.LoadModule("scripts/%s" % name)
316 for script in _autoconf.GNT_SCRIPTS:
317 self._CheckManpage(script,
318 self._ReadManFile(script),
319 self._LoadScript(script).commands.keys())
321 def _CheckManpage(self, script, mantext, commands):
325 pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
326 if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
330 msg=("Manpage for '%s' missing documentation for %s" %
331 (script, utils.CommaJoin(missing))))
334 if __name__ == "__main__":
335 testutils.GanetiTestProgram()