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):
90 """Check whether all hooks are documented.
93 hooksdoc = _ReadDocFile("hooks.rst")
95 # Reverse mapping from LU to opcode
96 lu2opcode = dict((lu, op)
97 for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
98 assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
99 "Found duplicate entries"
101 for name in dir(cmdlib):
102 obj = getattr(cmdlib, name)
104 if (isinstance(obj, type) and
105 issubclass(obj, cmdlib.LogicalUnit) and
106 hasattr(obj, "HPATH")):
107 self._CheckHook(name, obj, hooksdoc, lu2opcode)
109 def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
110 opcls = lu2opcode.get(lucls, None)
112 if lucls.HTYPE is None:
115 # TODO: Improve this test (e.g. find hooks documented but no longer
119 self.assertTrue(re.findall("^%s$" % re.escape(opcls.OP_ID),
121 msg=("Missing hook documentation for %s" %
124 pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)
126 self.assert_(re.findall(pattern, hooksdoc, re.M),
127 msg=("Missing documentation for hook %s/%s" %
128 (lucls.HTYPE, lucls.HPATH)))
131 class TestRapiDocs(unittest.TestCase):
132 def _CheckRapiResource(self, uri, fixup, handler):
133 docline = "%s resource." % uri
134 self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
135 msg=("First line of %r's docstring is not %r" %
138 # Apply fixes before testing
139 for (rx, value) in fixup.items():
140 uri = rx.sub(value, uri)
142 self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
145 """Check whether all RAPI resources are documented.
148 rapidoc = _ReadDocFile("rapi.rst")
150 node_name = re.escape("[node_name]")
151 instance_name = re.escape("[instance_name]")
152 group_name = re.escape("[group_name]")
153 job_id = re.escape("[job_id]")
154 disk_index = re.escape("[disk_index]")
155 query_res = re.escape("[resource]")
157 resources = connector.GetHandlers(node_name, instance_name, group_name,
158 job_id, disk_index, query_res)
160 handler_dups = utils.FindDuplicates(resources.values())
161 self.assertFalse(handler_dups,
162 msg=("Resource handlers used more than once: %r" %
166 re.compile(node_name): "node1examplecom",
167 re.compile(instance_name): "inst1examplecom",
168 re.compile(group_name): "group4440",
169 re.compile(job_id): "9409",
170 re.compile(disk_index): "123",
171 re.compile(query_res): "lock",
174 assert compat.all(VALID_URI_RE.match(value)
175 for value in uri_check_fixup.values()), \
176 "Fixup values must be valid URIs, too"
181 for line in rapidoc.splitlines():
182 if re.match(r"^\++$", line):
183 titles.append(prevline)
187 prefix_exception = frozenset(["/", "/version", "/2"])
192 for key, handler in resources.iteritems():
194 if hasattr(key, "match"):
195 self.assert_(key.pattern.startswith("^/2/"),
196 msg="Pattern %r does not start with '^/2/'" % key.pattern)
197 self.assertEqual(key.pattern[-1], "$")
201 if title.startswith("``") and title.endswith("``"):
204 self._CheckRapiResource(uri, uri_check_fixup, handler)
205 used_uris.append(uri)
210 # TODO: Find better way of identifying resource
211 undocumented.append(key.pattern)
214 self.assert_(key.startswith("/2/") or key in prefix_exception,
215 msg="Path %r does not start with '/2/'" % key)
217 if ("``%s``" % key) in titles:
218 self._CheckRapiResource(key, {}, handler)
219 used_uris.append(key)
221 undocumented.append(key)
223 self.failIf(undocumented,
224 msg=("Missing RAPI resource documentation for %s" %
225 utils.CommaJoin(undocumented)))
227 uri_dups = utils.FindDuplicates(used_uris)
228 self.failIf(uri_dups,
229 msg=("URIs matched by more than one resource: %s" %
230 utils.CommaJoin(uri_dups)))
232 self._FindRapiMissing(resources.values())
233 self._CheckTagHandlers(resources.values())
235 def _FindRapiMissing(self, handlers):
236 used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
239 unexpected = used & RAPI_OPCODE_EXCLUDE
240 self.assertFalse(unexpected,
241 msg=("Found RAPI resources for excluded opcodes: %s" %
242 utils.CommaJoin(_GetOpIds(unexpected))))
244 missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
246 self.assertFalse(missing,
247 msg=("Missing RAPI resources for opcodes: %s" %
248 utils.CommaJoin(_GetOpIds(missing))))
250 def _CheckTagHandlers(self, handlers):
251 tag_handlers = filter(lambda x: issubclass(x, rlib2._R_Tags), handlers)
252 self.assertEqual(frozenset(map(operator.attrgetter("TAG_LEVEL"),
254 constants.VALID_TAG_TYPES)
258 """Returns C{OP_ID} for all opcodes in passed sequence.
261 return sorted(opcls.OP_ID for opcls in ops)
264 class TestManpages(unittest.TestCase):
268 def _ReadManFile(name):
269 return utils.ReadFile("%s/man/%s.rst" %
270 (testutils.GetSourceDir(), name))
273 def _LoadScript(name):
274 return build.LoadModule("scripts/%s" % name)
277 for script in _autoconf.GNT_SCRIPTS:
278 self._CheckManpage(script,
279 self._ReadManFile(script),
280 self._LoadScript(script).commands.keys())
282 def _CheckManpage(self, script, mantext, commands):
286 pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
287 if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
291 msg=("Manpage for '%s' missing documentation for %s" %
292 (script, utils.CommaJoin(missing))))
295 if __name__ == "__main__":
296 testutils.GanetiTestProgram()