Statistics
| Branch: | Tag: | Revision:

root / test / docs_unittest.py @ 2fd213a6

History | View | Annotate | Download (9.9 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
    job_id = re.escape("[job_id]")
187
    disk_index = re.escape("[disk_index]")
188
    query_res = re.escape("[resource]")
189

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

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

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

    
207
    assert compat.all(VALID_URI_RE.match(value)
208
                      for value in uri_check_fixup.values()), \
209
           "Fixup values must be valid URIs, too"
210

    
211
    titles = []
212

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

    
218
      prevline = line
219

    
220
    prefix_exception = frozenset(["/", "/version", "/2"])
221

    
222
    undocumented = []
223
    used_uris = []
224

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

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

    
242
        if not found:
243
          # TODO: Find better way of identifying resource
244
          undocumented.append(key.pattern)
245

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

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

    
256
    self.failIf(undocumented,
257
                msg=("Missing RAPI resource documentation for %s" %
258
                     utils.CommaJoin(undocumented)))
259

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

    
265
    self._FindRapiMissing(resources.values())
266
    self._CheckTagHandlers(resources.values())
267

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

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

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

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

    
289

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

293
  """
294
  return sorted(opcls.OP_ID for opcls in ops)
295

    
296

    
297
class TestManpages(unittest.TestCase):
298
  """Manpage tests"""
299

    
300
  @staticmethod
301
  def _ReadManFile(name):
302
    return utils.ReadFile("%s/man/%s.rst" %
303
                          (testutils.GetSourceDir(), name))
304

    
305
  @staticmethod
306
  def _LoadScript(name):
307
    return build.LoadModule("scripts/%s" % name)
308

    
309
  def test(self):
310
    for script in _autoconf.GNT_SCRIPTS:
311
      self._CheckManpage(script,
312
                         self._ReadManFile(script),
313
                         self._LoadScript(script).commands.keys())
314

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

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

    
323
    self.failIf(missing,
324
                msg=("Manpage for '%s' missing documentation for %s" %
325
                     (script, utils.CommaJoin(missing))))
326

    
327

    
328
if __name__ == "__main__":
329
  testutils.GanetiTestProgram()