docs_unittest.py 8.82 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/python
#

# Copyright (C) 2009 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Script for unittesting documentation"""

import unittest
import re
26
27
import itertools
import operator
28

29
from ganeti import _autoconf
30
31
from ganeti import utils
from ganeti import cmdlib
32
from ganeti import build
33
from ganeti import compat
34
from ganeti import mcpu
35
36
37
38
from ganeti import opcodes
from ganeti import constants
from ganeti.rapi import baserlib
from ganeti.rapi import rlib2
39
from ganeti.rapi import connector
40
41
42
43

import testutils


44
45
VALID_URI_RE = re.compile(r"^[-/a-z0-9]*$")

46
47
48
49
50
51
52
53
54
55
56
57
58
RAPI_OPCODE_EXCLUDE = frozenset([
  # Not yet implemented
  opcodes.OpBackupQuery,
  opcodes.OpBackupRemove,
  opcodes.OpClusterConfigQuery,
  opcodes.OpClusterRepairDiskSizes,
  opcodes.OpClusterVerify,
  opcodes.OpClusterVerifyDisks,
  opcodes.OpInstanceChangeGroup,
  opcodes.OpInstanceMove,
  opcodes.OpNodeQueryvols,
  opcodes.OpOobCommand,
  opcodes.OpTagsSearch,
59
60
  opcodes.OpClusterActivateMasterIp,
  opcodes.OpClusterDeactivateMasterIp,
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

  # Difficult if not impossible
  opcodes.OpClusterDestroy,
  opcodes.OpClusterPostInit,
  opcodes.OpClusterRename,
  opcodes.OpNodeAdd,
  opcodes.OpNodeRemove,

  # Helper opcodes (e.g. submitted by LUs)
  opcodes.OpClusterVerifyConfig,
  opcodes.OpClusterVerifyGroup,
  opcodes.OpGroupEvacuate,
  opcodes.OpGroupVerifyDisks,

  # Test opcodes
  opcodes.OpTestAllocator,
  opcodes.OpTestDelay,
  opcodes.OpTestDummy,
  opcodes.OpTestJqueue,
  ])

82

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

87

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

    """
93
    hooksdoc = _ReadDocFile("hooks.rst")
94

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

101
102
103
104
105
106
    for name in dir(cmdlib):
      obj = getattr(cmdlib, name)

      if (isinstance(obj, type) and
          issubclass(obj, cmdlib.LogicalUnit) and
          hasattr(obj, "HPATH")):
107
108
109
110
        self._CheckHook(name, obj, hooksdoc, lu2opcode)

  def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
    opcls = lu2opcode.get(lucls, None)
111
112
113
114
115
116
117

    if lucls.HTYPE is None:
      return

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

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

124
125
126
127
128
129
    pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)

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

130
131

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

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

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

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

    """
148
    rapidoc = _ReadDocFile("rapi.rst")
149

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

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

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

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

174
175
176
    assert compat.all(VALID_URI_RE.match(value)
                      for value in uri_check_fixup.values()), \
           "Fixup values must be valid URIs, too"
177
178
179
180
181
182
183
184
185
186

    titles = []

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

      prevline = line

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

189
    undocumented = []
190
    used_uris = []
191
192
193
194

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

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

        if not found:
          # TODO: Find better way of identifying resource
211
212
213
214
215
          undocumented.append(key.pattern)

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

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

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

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

232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
    self._FindRapiMissing(resources.values())
    self._CheckTagHandlers(resources.values())

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

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

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

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


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

  """
  return sorted(opcls.OP_ID for opcls in ops)

263

264
265
266
267
268
class TestManpages(unittest.TestCase):
  """Manpage tests"""

  @staticmethod
  def _ReadManFile(name):
Iustin Pop's avatar
Iustin Pop committed
269
    return utils.ReadFile("%s/man/%s.rst" %
270
271
272
273
                          (testutils.GetSourceDir(), name))

  @staticmethod
  def _LoadScript(name):
274
    return build.LoadModule("scripts/%s" % name)
275
276
277
278
279
280
281
282
283
284
285

  def test(self):
    for script in _autoconf.GNT_SCRIPTS:
      self._CheckManpage(script,
                         self._ReadManFile(script),
                         self._LoadScript(script).commands.keys())

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

    for cmd in commands:
Iustin Pop's avatar
Iustin Pop committed
286
287
      pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
      if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
288
289
290
291
        missing.append(cmd)

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


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