RAPI: Fix resource for replacing disks
[ganeti-local] / test / docs_unittest.py
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
27 from ganeti import _autoconf
28 from ganeti import utils
29 from ganeti import cmdlib
30 from ganeti import build
31 from ganeti import compat
32 from ganeti import mcpu
33 from ganeti.rapi import connector
34
35 import testutils
36
37
38 VALID_URI_RE = re.compile(r"^[-/a-z0-9]*$")
39
40
41 class TestDocs(unittest.TestCase):
42   """Documentation tests"""
43
44   @staticmethod
45   def _ReadDocFile(filename):
46     return utils.ReadFile("%s/doc/%s" %
47                           (testutils.GetSourceDir(), filename))
48
49   def testHookDocs(self):
50     """Check whether all hooks are documented.
51
52     """
53     hooksdoc = self._ReadDocFile("hooks.rst")
54
55     # Reverse mapping from LU to opcode
56     lu2opcode = dict((lu, op)
57                      for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
58     assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
59       "Found duplicate entries"
60
61     for name in dir(cmdlib):
62       obj = getattr(cmdlib, name)
63
64       if (isinstance(obj, type) and
65           issubclass(obj, cmdlib.LogicalUnit) and
66           hasattr(obj, "HPATH")):
67         self._CheckHook(name, obj, hooksdoc, lu2opcode)
68
69   def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
70     opcls = lu2opcode.get(lucls, None)
71
72     if lucls.HTYPE is None:
73       return
74
75     # TODO: Improve this test (e.g. find hooks documented but no longer
76     # existing)
77
78     if opcls:
79       self.assertTrue(re.findall("^%s$" % re.escape(opcls.OP_ID),
80                                  hooksdoc, re.M),
81                       msg=("Missing hook documentation for %s" %
82                            (opcls.OP_ID)))
83
84     pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)
85
86     self.assert_(re.findall(pattern, hooksdoc, re.M),
87                  msg=("Missing documentation for hook %s/%s" %
88                       (lucls.HTYPE, lucls.HPATH)))
89
90   def _CheckRapiResource(self, uri, fixup, handler):
91     docline = "%s resource." % uri
92     self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
93                      msg=("First line of %r's docstring is not %r" %
94                           (handler, docline)))
95
96     # Apply fixes before testing
97     for (rx, value) in fixup.items():
98       uri = rx.sub(value, uri)
99
100     self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
101
102   def testRapiDocs(self):
103     """Check whether all RAPI resources are documented.
104
105     """
106     rapidoc = self._ReadDocFile("rapi.rst")
107
108     node_name = re.escape("[node_name]")
109     instance_name = re.escape("[instance_name]")
110     group_name = re.escape("[group_name]")
111     job_id = re.escape("[job_id]")
112     disk_index = re.escape("[disk_index]")
113     query_res = re.escape("[resource]")
114
115     resources = connector.GetHandlers(node_name, instance_name, group_name,
116                                       job_id, disk_index, query_res)
117
118     handler_dups = utils.FindDuplicates(resources.values())
119     self.assertFalse(handler_dups,
120                      msg=("Resource handlers used more than once: %r" %
121                           handler_dups))
122
123     uri_check_fixup = {
124       re.compile(node_name): "node1examplecom",
125       re.compile(instance_name): "inst1examplecom",
126       re.compile(group_name): "group4440",
127       re.compile(job_id): "9409",
128       re.compile(disk_index): "123",
129       re.compile(query_res): "lock",
130       }
131
132     assert compat.all(VALID_URI_RE.match(value)
133                       for value in uri_check_fixup.values()), \
134            "Fixup values must be valid URIs, too"
135
136     titles = []
137
138     prevline = None
139     for line in rapidoc.splitlines():
140       if re.match(r"^\++$", line):
141         titles.append(prevline)
142
143       prevline = line
144
145     prefix_exception = frozenset(["/", "/version", "/2"])
146
147     undocumented = []
148     used_uris = []
149
150     for key, handler in resources.iteritems():
151       # Regex objects
152       if hasattr(key, "match"):
153         self.assert_(key.pattern.startswith("^/2/"),
154                      msg="Pattern %r does not start with '^/2/'" % key.pattern)
155         self.assertEqual(key.pattern[-1], "$")
156
157         found = False
158         for title in titles:
159           if title.startswith("``") and title.endswith("``"):
160             uri = title[2:-2]
161             if key.match(uri):
162               self._CheckRapiResource(uri, uri_check_fixup, handler)
163               used_uris.append(uri)
164               found = True
165               break
166
167         if not found:
168           # TODO: Find better way of identifying resource
169           undocumented.append(key.pattern)
170
171       else:
172         self.assert_(key.startswith("/2/") or key in prefix_exception,
173                      msg="Path %r does not start with '/2/'" % key)
174
175         if ("``%s``" % key) in titles:
176           self._CheckRapiResource(key, {}, handler)
177           used_uris.append(key)
178         else:
179           undocumented.append(key)
180
181     self.failIf(undocumented,
182                 msg=("Missing RAPI resource documentation for %s" %
183                      utils.CommaJoin(undocumented)))
184
185     uri_dups = utils.FindDuplicates(used_uris)
186     self.failIf(uri_dups,
187                 msg=("URIs matched by more than one resource: %s" %
188                      utils.CommaJoin(uri_dups)))
189
190
191 class TestManpages(unittest.TestCase):
192   """Manpage tests"""
193
194   @staticmethod
195   def _ReadManFile(name):
196     return utils.ReadFile("%s/man/%s.rst" %
197                           (testutils.GetSourceDir(), name))
198
199   @staticmethod
200   def _LoadScript(name):
201     return build.LoadModule("scripts/%s" % name)
202
203   def test(self):
204     for script in _autoconf.GNT_SCRIPTS:
205       self._CheckManpage(script,
206                          self._ReadManFile(script),
207                          self._LoadScript(script).commands.keys())
208
209   def _CheckManpage(self, script, mantext, commands):
210     missing = []
211
212     for cmd in commands:
213       pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
214       if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
215         missing.append(cmd)
216
217     self.failIf(missing,
218                 msg=("Manpage for '%s' missing documentation for %s" %
219                      (script, utils.CommaJoin(missing))))
220
221
222 if __name__ == "__main__":
223   testutils.GanetiTestProgram()