root / test / docs_unittest.py @ b954f097
History | View | Annotate | Download (10.2 kB)
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() |