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