Statistics
| Branch: | Tag: | Revision:

root / test / docs_unittest.py @ fdaacd25

History | View | Annotate | Download (10.1 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.OpNodeQueryvols,
57
  opcodes.OpOobCommand,
58
  opcodes.OpTagsSearch,
59
  opcodes.OpClusterActivateMasterIp,
60
  opcodes.OpClusterDeactivateMasterIp,
61

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

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

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

    
82

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

    
87

    
88
class TestHooksDocs(unittest.TestCase):
89
  HOOK_PATH_OK = frozenset([
90
    "master-ip-turnup",
91
    "master-ip-turndown",
92
    ])
93

    
94
  def test(self):
95
    """Check whether all hooks are documented.
96

97
    """
98
    hooksdoc = _ReadDocFile("hooks.rst")
99

    
100
    # Reverse mapping from LU to opcode
101
    lu2opcode = dict((lu, op)
102
                     for (op, lu) in mcpu.Processor.DISPATCH_TABLE.items())
103
    assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
104
      "Found duplicate entries"
105

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

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

    
120
    self.assertFalse(duplicate_ops,
121
                     msg="Found duplicate opcode documentation: %s" %
122
                         utils.CommaJoin(duplicate_ops))
123

    
124
    seen_paths = set()
125
    seen_ops = set()
126

    
127
    self.assertFalse(duplicate_ops,
128
                     msg="Found duplicated hook documentation: %s" %
129
                         utils.CommaJoin(duplicate_ops))
130

    
131
    for name in dir(cmdlib):
132
      lucls = getattr(cmdlib, name)
133

    
134
      if (isinstance(lucls, type) and
135
          issubclass(lucls, cmdlib.LogicalUnit) and
136
          hasattr(lucls, "HPATH")):
137
        if lucls.HTYPE is None:
138
          continue
139

    
140
        opcls = lu2opcode.get(lucls, None)
141

    
142
        if opcls:
143
          seen_ops.add(opcls.OP_ID)
144
          self.assertTrue(opcls.OP_ID in hooks_ops,
145
                          msg="Missing hook documentation for %s" %
146
                              opcls.OP_ID)
147
        self.assertTrue(lucls.HPATH in hooks_paths,
148
                        msg="Missing documentation for hook %s/%s" %
149
                            (lucls.HTYPE, lucls.HPATH))
150
        seen_paths.add(lucls.HPATH)
151

    
152
    missed_ops = hooks_ops - seen_ops
153
    missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
154

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

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

    
163

    
164
class TestRapiDocs(unittest.TestCase):
165
  def _CheckRapiResource(self, uri, fixup, handler):
166
    docline = "%s resource." % uri
167
    self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
168
                     msg=("First line of %r's docstring is not %r" %
169
                          (handler, docline)))
170

    
171
    # Apply fixes before testing
172
    for (rx, value) in fixup.items():
173
      uri = rx.sub(value, uri)
174

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

    
177
  def test(self):
178
    """Check whether all RAPI resources are documented.
179

180
    """
181
    rapidoc = _ReadDocFile("rapi.rst")
182

    
183
    node_name = re.escape("[node_name]")
184
    instance_name = re.escape("[instance_name]")
185
    group_name = re.escape("[group_name]")
186
    network_name = re.escape("[network_name]")
187
    job_id = re.escape("[job_id]")
188
    disk_index = re.escape("[disk_index]")
189
    query_res = re.escape("[resource]")
190

    
191
    resources = connector.GetHandlers(node_name, instance_name,
192
                                      group_name, network_name,
193
                                      job_id, disk_index, query_res)
194

    
195
    handler_dups = utils.FindDuplicates(resources.values())
196
    self.assertFalse(handler_dups,
197
                     msg=("Resource handlers used more than once: %r" %
198
                          handler_dups))
199

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

    
210
    assert compat.all(VALID_URI_RE.match(value)
211
                      for value in uri_check_fixup.values()), \
212
           "Fixup values must be valid URIs, too"
213

    
214
    titles = []
215

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

    
221
      prevline = line
222

    
223
    prefix_exception = frozenset(["/", "/version", "/2"])
224

    
225
    undocumented = []
226
    used_uris = []
227

    
228
    for key, handler in resources.iteritems():
229
      # Regex objects
230
      if hasattr(key, "match"):
231
        self.assert_(key.pattern.startswith("^/2/"),
232
                     msg="Pattern %r does not start with '^/2/'" % key.pattern)
233
        self.assertEqual(key.pattern[-1], "$")
234

    
235
        found = False
236
        for title in titles:
237
          if title.startswith("``") and title.endswith("``"):
238
            uri = title[2:-2]
239
            if key.match(uri):
240
              self._CheckRapiResource(uri, uri_check_fixup, handler)
241
              used_uris.append(uri)
242
              found = True
243
              break
244

    
245
        if not found:
246
          # TODO: Find better way of identifying resource
247
          undocumented.append(key.pattern)
248

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

    
253
        if ("``%s``" % key) in titles:
254
          self._CheckRapiResource(key, {}, handler)
255
          used_uris.append(key)
256
        else:
257
          undocumented.append(key)
258

    
259
    self.failIf(undocumented,
260
                msg=("Missing RAPI resource documentation for %s" %
261
                     utils.CommaJoin(undocumented)))
262

    
263
    uri_dups = utils.FindDuplicates(used_uris)
264
    self.failIf(uri_dups,
265
                msg=("URIs matched by more than one resource: %s" %
266
                     utils.CommaJoin(uri_dups)))
267

    
268
    self._FindRapiMissing(resources.values())
269
    self._CheckTagHandlers(resources.values())
270

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

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

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

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

    
292

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

296
  """
297
  return sorted(opcls.OP_ID for opcls in ops)
298

    
299

    
300
class TestManpages(unittest.TestCase):
301
  """Manpage tests"""
302

    
303
  @staticmethod
304
  def _ReadManFile(name):
305
    return utils.ReadFile("%s/man/%s.rst" %
306
                          (testutils.GetSourceDir(), name))
307

    
308
  @staticmethod
309
  def _LoadScript(name):
310
    return build.LoadModule("scripts/%s" % name)
311

    
312
  def test(self):
313
    for script in _autoconf.GNT_SCRIPTS:
314
      self._CheckManpage(script,
315
                         self._ReadManFile(script),
316
                         self._LoadScript(script).commands.keys())
317

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

    
321
    for cmd in commands:
322
      pattern = r"^(\| )?\*\*%s\*\*" % re.escape(cmd)
323
      if not re.findall(pattern, mantext, re.DOTALL | re.MULTILINE):
324
        missing.append(cmd)
325

    
326
    self.failIf(missing,
327
                msg=("Manpage for '%s' missing documentation for %s" %
328
                     (script, utils.CommaJoin(missing))))
329

    
330

    
331
if __name__ == "__main__":
332
  testutils.GanetiTestProgram()