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 = compat.UniqueFrozenset([
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,
61 opcodes.OpExtStorageDiagnose,
63 # Difficult if not impossible
64 opcodes.OpClusterDestroy,
65 opcodes.OpClusterPostInit,
66 opcodes.OpClusterRename,
70 # Very sensitive in nature
71 opcodes.OpRestrictedCommand,
73 # Helper opcodes (e.g. submitted by LUs)
74 opcodes.OpClusterVerifyConfig,
75 opcodes.OpClusterVerifyGroup,
76 opcodes.OpGroupEvacuate,
77 opcodes.OpGroupVerifyDisks,
80 opcodes.OpTestAllocator,
87 def _ReadDocFile(filename):
88 return utils.ReadFile("%s/doc/%s" %
89 (testutils.GetSourceDir(), filename))
92 class TestHooksDocs(unittest.TestCase):
93 HOOK_PATH_OK = compat.UniqueFrozenset([
99 """Check whether all hooks are documented.
102 hooksdoc = _ReadDocFile("hooks.rst")
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"
110 hooks_paths = frozenset(re.findall("^:directory:\s*(.+)\s*$", hooksdoc,
112 self.assertTrue(self.HOOK_PATH_OK.issubset(hooks_paths),
113 msg="Whitelisted path not found in documentation")
115 raw_hooks_ops = re.findall("^OP_(?!CODE$).+$", hooksdoc, re.M)
117 duplicate_ops = set()
118 for op in raw_hooks_ops:
120 duplicate_ops.add(op)
124 self.assertFalse(duplicate_ops,
125 msg="Found duplicate opcode documentation: %s" %
126 utils.CommaJoin(duplicate_ops))
131 self.assertFalse(duplicate_ops,
132 msg="Found duplicated hook documentation: %s" %
133 utils.CommaJoin(duplicate_ops))
135 for name in dir(cmdlib):
136 lucls = getattr(cmdlib, name)
138 if (isinstance(lucls, type) and
139 issubclass(lucls, cmdlib.LogicalUnit) and
140 hasattr(lucls, "HPATH")):
141 if lucls.HTYPE is None:
144 opcls = lu2opcode.get(lucls, None)
147 seen_ops.add(opcls.OP_ID)
148 self.assertTrue(opcls.OP_ID in hooks_ops,
149 msg="Missing hook documentation for %s" %
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)
156 missed_ops = hooks_ops - seen_ops
157 missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
159 self.assertFalse(missed_ops,
160 msg="Op documents hook not existing anymore: %s" %
161 utils.CommaJoin(missed_ops))
163 self.assertFalse(missed_paths,
164 msg="Hook path does not exist in opcode: %s" %
165 utils.CommaJoin(missed_paths))
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" %
175 # Apply fixes before testing
176 for (rx, value) in fixup.items():
177 uri = rx.sub(value, uri)
179 self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
182 """Check whether all RAPI resources are documented.
185 rapidoc = _ReadDocFile("rapi.rst")
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]")
195 resources = connector.GetHandlers(node_name, instance_name,
196 group_name, network_name,
197 job_id, disk_index, query_res)
199 handler_dups = utils.FindDuplicates(resources.values())
200 self.assertFalse(handler_dups,
201 msg=("Resource handlers used more than once: %r" %
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",
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"
221 for line in rapidoc.splitlines():
222 if re.match(r"^\++$", line):
223 titles.append(prevline)
227 prefix_exception = compat.UniqueFrozenset(["/", "/version", "/2"])
232 for key, handler in resources.iteritems():
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], "$")
241 if title.startswith("``") and title.endswith("``"):
244 self._CheckRapiResource(uri, uri_check_fixup, handler)
245 used_uris.append(uri)
250 # TODO: Find better way of identifying resource
251 undocumented.append(key.pattern)
254 self.assert_(key.startswith("/2/") or key in prefix_exception,
255 msg="Path %r does not start with '/2/'" % key)
257 if ("``%s``" % key) in titles:
258 self._CheckRapiResource(key, {}, handler)
259 used_uris.append(key)
261 undocumented.append(key)
263 self.failIf(undocumented,
264 msg=("Missing RAPI resource documentation for %s" %
265 utils.CommaJoin(undocumented)))
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)))
272 self._FindRapiMissing(resources.values())
273 self._CheckTagHandlers(resources.values())
275 def _FindRapiMissing(self, handlers):
276 used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
279 unexpected = used & RAPI_OPCODE_EXCLUDE
280 self.assertFalse(unexpected,
281 msg=("Found RAPI resources for excluded opcodes: %s" %
282 utils.CommaJoin(_GetOpIds(unexpected))))
284 missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
286 self.assertFalse(missing,
287 msg=("Missing RAPI resources for opcodes: %s" %
288 utils.CommaJoin(_GetOpIds(missing))))
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"),
294 constants.VALID_TAG_TYPES)
298 """Returns C{OP_ID} for all opcodes in passed sequence.
301 return sorted(opcls.OP_ID for opcls in ops)
304 class TestManpages(unittest.TestCase):
308 def _ReadManFile(name):
309 return utils.ReadFile("%s/man/%s.rst" %
310 (testutils.GetSourceDir(), name))
313 def _LoadScript(name):
314 return build.LoadModule("scripts/%s" % name)
317 for script in _autoconf.GNT_SCRIPTS:
318 self._CheckManpage(script,
319 self._ReadManFile(script),
320 self._LoadScript(script).commands.keys())
322 def _CheckManpage(self, script, mantext, commands):
326 pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
327 if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
331 msg=("Manpage for '%s' missing documentation for %s" %
332 (script, utils.CommaJoin(missing))))
335 if __name__ == "__main__":
336 testutils.GanetiTestProgram()