Statistics
| Branch: | Tag: | Revision:

root / test / docs_unittest.py @ a52978c7

History | View | Annotate | Download (8.8 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 = frozenset([
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.OpNodePowercycle,
57
  opcodes.OpNodeQueryvols,
58
  opcodes.OpOobCommand,
59
  opcodes.OpTagsSearch,
60

    
61
  # Difficult if not impossible
62
  opcodes.OpClusterDestroy,
63
  opcodes.OpClusterPostInit,
64
  opcodes.OpClusterRename,
65
  opcodes.OpNodeAdd,
66
  opcodes.OpNodeRemove,
67

    
68
  # Helper opcodes (e.g. submitted by LUs)
69
  opcodes.OpClusterVerifyConfig,
70
  opcodes.OpClusterVerifyGroup,
71
  opcodes.OpGroupEvacuate,
72
  opcodes.OpGroupVerifyDisks,
73

    
74
  # Test opcodes
75
  opcodes.OpTestAllocator,
76
  opcodes.OpTestDelay,
77
  opcodes.OpTestDummy,
78
  opcodes.OpTestJqueue,
79
  ])
80

    
81

    
82
def _ReadDocFile(filename):
83
  return utils.ReadFile("%s/doc/%s" %
84
                        (testutils.GetSourceDir(), filename))
85

    
86

    
87
class TestHooksDocs(unittest.TestCase):
88
  def test(self):
89
    """Check whether all hooks are documented.
90

91
    """
92
    hooksdoc = _ReadDocFile("hooks.rst")
93

    
94
    # Reverse mapping from LU to opcode
95
    lu2opcode = dict((lu, op)
96
                     for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
97
    assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
98
      "Found duplicate entries"
99

    
100
    for name in dir(cmdlib):
101
      obj = getattr(cmdlib, name)
102

    
103
      if (isinstance(obj, type) and
104
          issubclass(obj, cmdlib.LogicalUnit) and
105
          hasattr(obj, "HPATH")):
106
        self._CheckHook(name, obj, hooksdoc, lu2opcode)
107

    
108
  def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
109
    opcls = lu2opcode.get(lucls, None)
110

    
111
    if lucls.HTYPE is None:
112
      return
113

    
114
    # TODO: Improve this test (e.g. find hooks documented but no longer
115
    # existing)
116

    
117
    if opcls:
118
      self.assertTrue(re.findall("^%s$" % re.escape(opcls.OP_ID),
119
                                 hooksdoc, re.M),
120
                      msg=("Missing hook documentation for %s" %
121
                           (opcls.OP_ID)))
122

    
123
    pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)
124

    
125
    self.assert_(re.findall(pattern, hooksdoc, re.M),
126
                 msg=("Missing documentation for hook %s/%s" %
127
                      (lucls.HTYPE, lucls.HPATH)))
128

    
129

    
130
class TestRapiDocs(unittest.TestCase):
131
  def _CheckRapiResource(self, uri, fixup, handler):
132
    docline = "%s resource." % uri
133
    self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
134
                     msg=("First line of %r's docstring is not %r" %
135
                          (handler, docline)))
136

    
137
    # Apply fixes before testing
138
    for (rx, value) in fixup.items():
139
      uri = rx.sub(value, uri)
140

    
141
    self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
142

    
143
  def test(self):
144
    """Check whether all RAPI resources are documented.
145

146
    """
147
    rapidoc = _ReadDocFile("rapi.rst")
148

    
149
    node_name = re.escape("[node_name]")
150
    instance_name = re.escape("[instance_name]")
151
    group_name = re.escape("[group_name]")
152
    job_id = re.escape("[job_id]")
153
    disk_index = re.escape("[disk_index]")
154
    query_res = re.escape("[resource]")
155

    
156
    resources = connector.GetHandlers(node_name, instance_name, group_name,
157
                                      job_id, disk_index, query_res)
158

    
159
    handler_dups = utils.FindDuplicates(resources.values())
160
    self.assertFalse(handler_dups,
161
                     msg=("Resource handlers used more than once: %r" %
162
                          handler_dups))
163

    
164
    uri_check_fixup = {
165
      re.compile(node_name): "node1examplecom",
166
      re.compile(instance_name): "inst1examplecom",
167
      re.compile(group_name): "group4440",
168
      re.compile(job_id): "9409",
169
      re.compile(disk_index): "123",
170
      re.compile(query_res): "lock",
171
      }
172

    
173
    assert compat.all(VALID_URI_RE.match(value)
174
                      for value in uri_check_fixup.values()), \
175
           "Fixup values must be valid URIs, too"
176

    
177
    titles = []
178

    
179
    prevline = None
180
    for line in rapidoc.splitlines():
181
      if re.match(r"^\++$", line):
182
        titles.append(prevline)
183

    
184
      prevline = line
185

    
186
    prefix_exception = frozenset(["/", "/version", "/2"])
187

    
188
    undocumented = []
189
    used_uris = []
190

    
191
    for key, handler in resources.iteritems():
192
      # Regex objects
193
      if hasattr(key, "match"):
194
        self.assert_(key.pattern.startswith("^/2/"),
195
                     msg="Pattern %r does not start with '^/2/'" % key.pattern)
196
        self.assertEqual(key.pattern[-1], "$")
197

    
198
        found = False
199
        for title in titles:
200
          if title.startswith("``") and title.endswith("``"):
201
            uri = title[2:-2]
202
            if key.match(uri):
203
              self._CheckRapiResource(uri, uri_check_fixup, handler)
204
              used_uris.append(uri)
205
              found = True
206
              break
207

    
208
        if not found:
209
          # TODO: Find better way of identifying resource
210
          undocumented.append(key.pattern)
211

    
212
      else:
213
        self.assert_(key.startswith("/2/") or key in prefix_exception,
214
                     msg="Path %r does not start with '/2/'" % key)
215

    
216
        if ("``%s``" % key) in titles:
217
          self._CheckRapiResource(key, {}, handler)
218
          used_uris.append(key)
219
        else:
220
          undocumented.append(key)
221

    
222
    self.failIf(undocumented,
223
                msg=("Missing RAPI resource documentation for %s" %
224
                     utils.CommaJoin(undocumented)))
225

    
226
    uri_dups = utils.FindDuplicates(used_uris)
227
    self.failIf(uri_dups,
228
                msg=("URIs matched by more than one resource: %s" %
229
                     utils.CommaJoin(uri_dups)))
230

    
231
    self._FindRapiMissing(resources.values())
232
    self._CheckTagHandlers(resources.values())
233

    
234
  def _FindRapiMissing(self, handlers):
235
    used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
236
                                          handlers)))
237

    
238
    unexpected = used & RAPI_OPCODE_EXCLUDE
239
    self.assertFalse(unexpected,
240
      msg=("Found RAPI resources for excluded opcodes: %s" %
241
           utils.CommaJoin(_GetOpIds(unexpected))))
242

    
243
    missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
244
               RAPI_OPCODE_EXCLUDE)
245
    self.assertFalse(missing,
246
      msg=("Missing RAPI resources for opcodes: %s" %
247
           utils.CommaJoin(_GetOpIds(missing))))
248

    
249
  def _CheckTagHandlers(self, handlers):
250
    tag_handlers = filter(lambda x: issubclass(x, rlib2._R_Tags), handlers)
251
    self.assertEqual(frozenset(map(operator.attrgetter("TAG_LEVEL"),
252
                                   tag_handlers)),
253
                     constants.VALID_TAG_TYPES)
254

    
255

    
256
def _GetOpIds(ops):
257
  """Returns C{OP_ID} for all opcodes in passed sequence.
258

259
  """
260
  return sorted(opcls.OP_ID for opcls in ops)
261

    
262

    
263
class TestManpages(unittest.TestCase):
264
  """Manpage tests"""
265

    
266
  @staticmethod
267
  def _ReadManFile(name):
268
    return utils.ReadFile("%s/man/%s.rst" %
269
                          (testutils.GetSourceDir(), name))
270

    
271
  @staticmethod
272
  def _LoadScript(name):
273
    return build.LoadModule("scripts/%s" % name)
274

    
275
  def test(self):
276
    for script in _autoconf.GNT_SCRIPTS:
277
      self._CheckManpage(script,
278
                         self._ReadManFile(script),
279
                         self._LoadScript(script).commands.keys())
280

    
281
  def _CheckManpage(self, script, mantext, commands):
282
    missing = []
283

    
284
    for cmd in commands:
285
      pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
286
      if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
287
        missing.append(cmd)
288

    
289
    self.failIf(missing,
290
                msg=("Manpage for '%s' missing documentation for %s" %
291
                     (script, utils.CommaJoin(missing))))
292

    
293

    
294
if __name__ == "__main__":
295
  testutils.GanetiTestProgram()