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 # Helper opcodes (e.g. submitted by LUs)
70 opcodes.OpClusterVerifyConfig,
71 opcodes.OpClusterVerifyGroup,
72 opcodes.OpGroupEvacuate,
73 opcodes.OpGroupVerifyDisks,
76 opcodes.OpTestAllocator,
83 def _ReadDocFile(filename):
84 return utils.ReadFile("%s/doc/%s" %
85 (testutils.GetSourceDir(), filename))
88 class TestHooksDocs(unittest.TestCase):
89 HOOK_PATH_OK = frozenset([
95 """Check whether all hooks are documented.
98 hooksdoc = _ReadDocFile("hooks.rst")
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"
106 hooks_paths = frozenset(re.findall("^:directory:\s*(.+)\s*$", hooksdoc,
108 self.assertTrue(self.HOOK_PATH_OK.issubset(hooks_paths),
109 msg="Whitelisted path not found in documentation")
111 raw_hooks_ops = re.findall("^OP_(?!CODE$).+$", hooksdoc, re.M)
113 duplicate_ops = set()
114 for op in raw_hooks_ops:
116 duplicate_ops.add(op)
120 self.assertFalse(duplicate_ops,
121 msg="Found duplicate opcode documentation: %s" %
122 utils.CommaJoin(duplicate_ops))
127 self.assertFalse(duplicate_ops,
128 msg="Found duplicated hook documentation: %s" %
129 utils.CommaJoin(duplicate_ops))
131 for name in dir(cmdlib):
132 lucls = getattr(cmdlib, name)
134 if (isinstance(lucls, type) and
135 issubclass(lucls, cmdlib.LogicalUnit) and
136 hasattr(lucls, "HPATH")):
137 if lucls.HTYPE is None:
140 opcls = lu2opcode.get(lucls, None)
143 seen_ops.add(opcls.OP_ID)
144 self.assertTrue(opcls.OP_ID in hooks_ops,
145 msg="Missing hook documentation for %s" %
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)
152 missed_ops = hooks_ops - seen_ops
153 missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
155 self.assertFalse(missed_ops,
156 msg="Op documents hook not existing anymore: %s" %
157 utils.CommaJoin(missed_ops))
159 self.assertFalse(missed_paths,
160 msg="Hook path does not exist in opcode: %s" %
161 utils.CommaJoin(missed_paths))
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" %
171 # Apply fixes before testing
172 for (rx, value) in fixup.items():
173 uri = rx.sub(value, uri)
175 self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
178 """Check whether all RAPI resources are documented.
181 rapidoc = _ReadDocFile("rapi.rst")
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]")
190 resources = connector.GetHandlers(node_name, instance_name, group_name,
191 job_id, disk_index, query_res)
193 handler_dups = utils.FindDuplicates(resources.values())
194 self.assertFalse(handler_dups,
195 msg=("Resource handlers used more than once: %r" %
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",
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"
214 for line in rapidoc.splitlines():
215 if re.match(r"^\++$", line):
216 titles.append(prevline)
220 prefix_exception = frozenset(["/", "/version", "/2"])
225 for key, handler in resources.iteritems():
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], "$")
234 if title.startswith("``") and title.endswith("``"):
237 self._CheckRapiResource(uri, uri_check_fixup, handler)
238 used_uris.append(uri)
243 # TODO: Find better way of identifying resource
244 undocumented.append(key.pattern)
247 self.assert_(key.startswith("/2/") or key in prefix_exception,
248 msg="Path %r does not start with '/2/'" % key)
250 if ("``%s``" % key) in titles:
251 self._CheckRapiResource(key, {}, handler)
252 used_uris.append(key)
254 undocumented.append(key)
256 self.failIf(undocumented,
257 msg=("Missing RAPI resource documentation for %s" %
258 utils.CommaJoin(undocumented)))
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)))
265 self._FindRapiMissing(resources.values())
266 self._CheckTagHandlers(resources.values())
268 def _FindRapiMissing(self, handlers):
269 used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
272 unexpected = used & RAPI_OPCODE_EXCLUDE
273 self.assertFalse(unexpected,
274 msg=("Found RAPI resources for excluded opcodes: %s" %
275 utils.CommaJoin(_GetOpIds(unexpected))))
277 missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
279 self.assertFalse(missing,
280 msg=("Missing RAPI resources for opcodes: %s" %
281 utils.CommaJoin(_GetOpIds(missing))))
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"),
287 constants.VALID_TAG_TYPES)
291 """Returns C{OP_ID} for all opcodes in passed sequence.
294 return sorted(opcls.OP_ID for opcls in ops)
297 class TestManpages(unittest.TestCase):
301 def _ReadManFile(name):
302 return utils.ReadFile("%s/man/%s.rst" %
303 (testutils.GetSourceDir(), name))
306 def _LoadScript(name):
307 return build.LoadModule("scripts/%s" % name)
310 for script in _autoconf.GNT_SCRIPTS:
311 self._CheckManpage(script,
312 self._ReadManFile(script),
313 self._LoadScript(script).commands.keys())
315 def _CheckManpage(self, script, mantext, commands):
319 pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
320 if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
324 msg=("Manpage for '%s' missing documentation for %s" %
325 (script, utils.CommaJoin(missing))))
328 if __name__ == "__main__":
329 testutils.GanetiTestProgram()