Statistics
| Branch: | Tag: | Revision:

root / test / docs_unittest.py @ b954f097

History | View | Annotate | Download (10.2 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 = compat.UniqueFrozenset([
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
  opcodes.OpExtStorageDiagnose,
62

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

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

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

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

    
86

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

    
91

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
167

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

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

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

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

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

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

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

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

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

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

    
218
    titles = []
219

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

    
225
      prevline = line
226

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

    
229
    undocumented = []
230
    used_uris = []
231

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

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

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

    
253
      else:
254
        self.assert_(key.startswith("/2/") or key in prefix_exception,
255
                     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
    self.failIf(undocumented,
264
                msg=("Missing RAPI resource documentation for %s" %
265
                     utils.CommaJoin(undocumented)))
266

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

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

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

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

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

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

    
296

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

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

    
303

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

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

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

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

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

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

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

    
334

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