docs_unittest.py 10.2 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 _constants
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
RAPI_OPCODE_EXCLUDE = compat.UniqueFrozenset([
47
48
49
50
51
52
53
54
55
56
57
58
  # 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
  opcodes.OpExtStorageDiagnose,
62
63
64
65
66
67
68
69

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

70
71
72
  # Very sensitive in nature
  opcodes.OpRestrictedCommand,

73
74
75
76
77
78
79
80
81
82
83
84
85
  # 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,
  ])

86

87
88
89
90
def _ReadDocFile(filename):
  return utils.ReadFile("%s/doc/%s" %
                        (testutils.GetSourceDir(), filename))

91

92
class TestHooksDocs(unittest.TestCase):
93
  HOOK_PATH_OK = compat.UniqueFrozenset([
94
95
96
97
    "master-ip-turnup",
    "master-ip-turndown",
    ])

98
  def test(self):
99
100
101
    """Check whether all hooks are documented.

    """
102
    hooksdoc = _ReadDocFile("hooks.rst")
103

104
105
106
107
108
109
    # 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"

110
111
112
113
    hooks_paths = frozenset(re.findall("^:directory:\s*(.+)\s*$", hooksdoc,
                                       re.M))
    self.assertTrue(self.HOOK_PATH_OK.issubset(hooks_paths),
                    msg="Whitelisted path not found in documentation")
114

115
116
117
118
119
120
121
122
    raw_hooks_ops = re.findall("^OP_(?!CODE$).+$", hooksdoc, re.M)
    hooks_ops = set()
    duplicate_ops = set()
    for op in raw_hooks_ops:
      if op in hooks_ops:
        duplicate_ops.add(op)
      else:
        hooks_ops.add(op)
123

124
125
126
    self.assertFalse(duplicate_ops,
                     msg="Found duplicate opcode documentation: %s" %
                         utils.CommaJoin(duplicate_ops))
127

128
129
    seen_paths = set()
    seen_ops = set()
130

131
132
133
    self.assertFalse(duplicate_ops,
                     msg="Found duplicated hook documentation: %s" %
                         utils.CommaJoin(duplicate_ops))
134

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
    for name in dir(cmdlib):
      lucls = getattr(cmdlib, name)

      if (isinstance(lucls, type) and
          issubclass(lucls, cmdlib.LogicalUnit) and
          hasattr(lucls, "HPATH")):
        if lucls.HTYPE is None:
          continue

        opcls = lu2opcode.get(lucls, None)

        if opcls:
          seen_ops.add(opcls.OP_ID)
          self.assertTrue(opcls.OP_ID in hooks_ops,
                          msg="Missing hook documentation for %s" %
                              opcls.OP_ID)
        self.assertTrue(lucls.HPATH in hooks_paths,
                        msg="Missing documentation for hook %s/%s" %
                            (lucls.HTYPE, lucls.HPATH))
        seen_paths.add(lucls.HPATH)

    missed_ops = hooks_ops - seen_ops
    missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK

    self.assertFalse(missed_ops,
                     msg="Op documents hook not existing anymore: %s" %
                         utils.CommaJoin(missed_ops))

    self.assertFalse(missed_paths,
                     msg="Hook path does not exist in opcode: %s" %
                         utils.CommaJoin(missed_paths))
166

167
168

class TestRapiDocs(unittest.TestCase):
169
170
171
172
173
174
  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)))

175
176
177
178
179
    # 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)
180

181
  def test(self):
182
183
184
    """Check whether all RAPI resources are documented.

    """
185
    rapidoc = _ReadDocFile("rapi.rst")
186

187
188
189
    node_name = re.escape("[node_name]")
    instance_name = re.escape("[instance_name]")
    group_name = re.escape("[group_name]")
190
    network_name = re.escape("[network_name]")
191
192
    job_id = re.escape("[job_id]")
    disk_index = re.escape("[disk_index]")
193
    query_res = re.escape("[resource]")
194

195
196
    resources = connector.GetHandlers(node_name, instance_name,
                                      group_name, network_name,
197
                                      job_id, disk_index, query_res)
198

199
200
201
202
203
    handler_dups = utils.FindDuplicates(resources.values())
    self.assertFalse(handler_dups,
                     msg=("Resource handlers used more than once: %r" %
                          handler_dups))

204
205
206
207
    uri_check_fixup = {
      re.compile(node_name): "node1examplecom",
      re.compile(instance_name): "inst1examplecom",
      re.compile(group_name): "group4440",
208
      re.compile(network_name): "network5550",
209
210
      re.compile(job_id): "9409",
      re.compile(disk_index): "123",
211
      re.compile(query_res): "lock",
212
      }
213

214
215
216
    assert compat.all(VALID_URI_RE.match(value)
                      for value in uri_check_fixup.values()), \
           "Fixup values must be valid URIs, too"
217
218
219
220
221
222
223
224
225
226

    titles = []

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

      prevline = line

227
    prefix_exception = compat.UniqueFrozenset(["/", "/version", "/2"])
228

229
    undocumented = []
230
    used_uris = []
231
232
233
234

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

239
240
        found = False
        for title in titles:
241
242
243
          if title.startswith("``") and title.endswith("``"):
            uri = title[2:-2]
            if key.match(uri):
244
              self._CheckRapiResource(uri, uri_check_fixup, handler)
245
              used_uris.append(uri)
246
247
              found = True
              break
248
249
250

        if not found:
          # TODO: Find better way of identifying resource
251
252
253
254
255
          undocumented.append(key.pattern)

      else:
        self.assert_(key.startswith("/2/") or key in prefix_exception,
                     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
264

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

267
268
269
270
271
    uri_dups = utils.FindDuplicates(used_uris)
    self.failIf(uri_dups,
                msg=("URIs matched by more than one resource: %s" %
                     utils.CommaJoin(uri_dups)))

272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
    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)

303

304
305
306
307
308
class TestManpages(unittest.TestCase):
  """Manpage tests"""

  @staticmethod
  def _ReadManFile(name):
Iustin Pop's avatar
Iustin Pop committed
309
    return utils.ReadFile("%s/man/%s.rst" %
310
311
312
313
                          (testutils.GetSourceDir(), name))

  @staticmethod
  def _LoadScript(name):
314
    return build.LoadModule("scripts/%s" % name)
315
316

  def test(self):
317
    for script in _constants.GNT_SCRIPTS:
318
319
320
321
322
323
324
325
      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
326
327
      pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
      if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
328
329
330
331
        missing.append(cmd)

    self.failIf(missing,
                msg=("Manpage for '%s' missing documentation for %s" %
332
                     (script, utils.CommaJoin(missing))))
333
334


335
if __name__ == "__main__":
336
  testutils.GanetiTestProgram()