Statistics
| Branch: | Tag: | Revision:

root / test / docs_unittest.py @ 6e8091f9

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
  # Very sensitive in nature
70
  opcodes.OpRestrictedCommand,
71

    
72
  # Helper opcodes (e.g. submitted by LUs)
73
  opcodes.OpClusterVerifyConfig,
74
  opcodes.OpClusterVerifyGroup,
75
  opcodes.OpGroupEvacuate,
76
  opcodes.OpGroupVerifyDisks,
77

    
78
  # Test opcodes
79
  opcodes.OpTestAllocator,
80
  opcodes.OpTestDelay,
81
  opcodes.OpTestDummy,
82
  opcodes.OpTestJqueue,
83
  ])
84

    
85

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

    
90

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

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

100
    """
101
    hooksdoc = _ReadDocFile("hooks.rst")
102

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

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

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

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

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

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

    
134
    for name in dir(cmdlib):
135
      lucls = getattr(cmdlib, name)
136

    
137
      if (isinstance(lucls, type) and
138
          issubclass(lucls, cmdlib.LogicalUnit) and
139
          hasattr(lucls, "HPATH")):
140
        if lucls.HTYPE is None:
141
          continue
142

    
143
        opcls = lu2opcode.get(lucls, None)
144

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

    
155
    missed_ops = hooks_ops - seen_ops
156
    missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
157

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

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

    
166

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

    
174
    # Apply fixes before testing
175
    for (rx, value) in fixup.items():
176
      uri = rx.sub(value, uri)
177

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

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

183
    """
184
    rapidoc = _ReadDocFile("rapi.rst")
185

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

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

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

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

    
213
    assert compat.all(VALID_URI_RE.match(value)
214
                      for value in uri_check_fixup.values()), \
215
           "Fixup values must be valid URIs, too"
216

    
217
    titles = []
218

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

    
224
      prevline = line
225

    
226
    prefix_exception = frozenset(["/", "/version", "/2"])
227

    
228
    undocumented = []
229
    used_uris = []
230

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

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

    
248
        if not found:
249
          # TODO: Find better way of identifying resource
250
          undocumented.append(key.pattern)
251

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

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

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

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

    
271
    self._FindRapiMissing(resources.values())
272
    self._CheckTagHandlers(resources.values())
273

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

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

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

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

    
295

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

299
  """
300
  return sorted(opcls.OP_ID for opcls in ops)
301

    
302

    
303
class TestManpages(unittest.TestCase):
304
  """Manpage tests"""
305

    
306
  @staticmethod
307
  def _ReadManFile(name):
308
    return utils.ReadFile("%s/man/%s.rst" %
309
                          (testutils.GetSourceDir(), name))
310

    
311
  @staticmethod
312
  def _LoadScript(name):
313
    return build.LoadModule("scripts/%s" % name)
314

    
315
  def test(self):
316
    for script in _autoconf.GNT_SCRIPTS:
317
      self._CheckManpage(script,
318
                         self._ReadManFile(script),
319
                         self._LoadScript(script).commands.keys())
320

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

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

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

    
333

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