3
# Rob Crittenden <rcritten@redhat.com>
4
# John Dennis <jdennis@redhat.com>
6
# Copyright (C) 2011 Red Hat
7
# see file 'COPYING' for use and warranty information
9
# This program is free software; you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License as published by
11
# the Free Software Foundation, either version 3 of the License, or
12
# (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
19
# You should have received a copy of the GNU General Public License
20
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
# Test the API against a known-good API to ensure that changes aren't made
30
from ipalib.text import Gettext, NGettext
34
API_FILE_DIFFERENCE = 1
40
from optparse import OptionParser
42
parser = OptionParser()
43
parser.add_option("--validate", dest="validate", action="store_true",
44
default=False, help="Validate the API vs the stored API")
46
parser.add_option("--no-validate-doc", dest="validate_doc", action="store_false",
47
default=True, help="Do not validate documentation")
49
options, args = parser.parse_args()
54
Remove the doc= part from the repr() of a Parameter.
57
# this pattern allows up to 2 nested parentheses in doc part
58
newline = re.sub(r', doc=([^(,]+)(\([^()]*(\([^()]+\)[^()]*)?\))?', '', line)
64
Iterate over all API commands and perform the following validation:
66
* Every command must have documentation
67
and it must be marked for international translation
69
* Every module hosting a command must have documentation
70
and it must be marked for international translation
72
* Every module topic must be marked for international translation
74
For every error found emit a diagnostic.
75
Emit a summary of total errors found.
77
Return error flag if errors found, zero otherwise.
81
'Helper utility to determine if object has been internationalized'
82
return isinstance(obj, (Gettext, NGettext))
87
# Used to track if we've processed a module already
90
# Initialize error counters
92
n_missing_cmd_i18n = 0
94
n_missing_mod_i18n = 0
96
# Iterate over every command
97
for cmd in api.Command():
98
cmd_class = cmd.__class__
100
# Skip commands marked as NO_CLI
101
if getattr(cmd, 'NO_CLI', False):
104
# Have we processed this module yet?
105
if not modules.setdefault(cmd.module, 0):
106
# First time seeing this module, validate the module contents
107
mod = sys.modules[cmd.module]
109
# See if there is a module topic, if so validate it
110
topic = getattr(mod, 'topic', None)
111
if topic is not None:
112
if not is_i18n(topic[1]):
113
src_file = inspect.getsourcefile(cmd_class)
114
n_missing_mod_i18n += 1
115
print "%s: topic in module \"%s\" is not internationalized" % \
116
(src_file, cmd.module)
118
# Does the module have documentation?
119
if mod.__doc__ is None:
120
src_file = inspect.getsourcefile(mod)
121
n_missing_mod_doc += 1
122
print "%s: module \"%s\" has no doc" % \
123
(src_file, cmd.module)
124
# Yes the module has doc, but is it internationalized?
125
elif not is_i18n(mod.__doc__):
126
src_file = inspect.getsourcefile(cmd_class)
127
n_missing_mod_i18n += 1
128
print "%s: module \"%s\" doc is not internationalized" % \
129
(src_file, cmd.module)
131
# Increment the count of how many commands in this module
132
modules[cmd.module] = modules[cmd.module] + 1
134
# Does the command have documentation?
135
if cmd.__doc__ is None:
136
src_file = inspect.getsourcefile(cmd_class)
137
line_num = inspect.getsourcelines(cmd_class)[1]
138
n_missing_cmd_doc += 1
139
print "%s:%d command \"%s\" has no doc" % (src_file, line_num, cmd.name)
140
# Yes the command has doc, but is it internationalized?
141
elif not is_i18n(cmd.__doc__):
142
src_file = inspect.getsourcefile(cmd_class)
143
line_num = inspect.getsourcelines(cmd_class)[1]
144
n_missing_cmd_i18n += 1
145
print "%s:%d command \"%s\" doc is not internationalized" % (src_file, line_num, cmd.name)
147
# If any errors, emit summary information and adjust return value
148
if n_missing_cmd_doc > 0 or n_missing_cmd_i18n > 0:
150
print "%d commands without doc, %d commands whose doc is not i18n" % \
151
(n_missing_cmd_doc, n_missing_cmd_i18n)
153
if n_missing_mod_doc > 0 or n_missing_mod_i18n > 0:
155
print "%d modules without doc, %d modules whose doc is not i18n" % \
156
(n_missing_mod_doc, n_missing_mod_i18n)
162
Write a new API file from the current tree.
164
fd = open(API_FILE, 'w')
165
for cmd in api.Command():
166
fd.write('command: %s\n' % cmd.name)
167
fd.write('args: %d,%d,%d\n' % (len(cmd.args), len(cmd.options), len(cmd.output)))
169
fd.write('arg: %s\n' % strip_doc(repr(a)))
170
for o in cmd.options():
171
fd.write('option: %s\n' % strip_doc(repr(o)))
172
for o in cmd.output():
173
fd.write('output: %s\n' % strip_doc(repr(o)))
180
Break apart a Param line and pull out the name. It would be nice if we
181
could just eval() the line but we wouldn't have defined any validators
182
or normalizers it may be using.
184
m = re.match('^[a-zA-Z0-9]+\(\'([a-z][_a-z0-9?\*\+]*)\'.*', line)
188
print "Couldn't find name in: %s" % line
192
def _finalize_command_validation(cmd, found_args, expected_args,
193
found_options, expected_options,
194
found_output, expected_output):
196
# Check the args of the previous command.
197
if len(found_args) != expected_args:
198
print 'Argument count in %s of %d doesn\'t match expected: %d' % (
199
cmd.name, len(found_args), expected_args)
201
if len(found_options) != expected_options:
202
print 'Options count in %s of %d doesn\'t match expected: %d' % (
203
cmd.name, len(found_options), expected_options)
205
if len(found_output) != expected_output:
206
print 'Output count in %s of %d doesn\'t match expected: %d' % (
207
cmd.name, len(found_output), expected_output)
210
# Check if there is not a new arg/opt/output in previous command
212
if a.param_spec not in found_args:
213
print 'Argument %s of command %s in ipalib, not in API file:\n%s' % (
214
a.param_spec, cmd.name, strip_doc(repr(a)))
216
for o in cmd.options():
217
if o.param_spec not in found_options:
218
print 'Option %s of command %s in ipalib, not in API file:\n%s' % (
219
o.param_spec, cmd.name, strip_doc(repr(o)))
221
for o in cmd.output():
222
if o.name not in found_output:
223
print 'Output %s of command %s in ipalib, not in API file:\n%s' % (
224
o.name, cmd.name, strip_doc(repr(o)))
231
Compare the API in the file to the one in ipalib.
233
Return a bitwise return code to identify the types of errors found, if
236
fd = open(API_FILE, 'r')
237
lines = fd.readlines()
249
# First run through the file and compare it to the API
254
if line.startswith('command:'):
256
if not _finalize_command_validation(cmd, found_args, expected_args,
257
found_options, expected_options,
258
found_output, expected_output):
259
rval |= API_FILE_DIFFERENCE
261
(arg, name) = line.split(': ', 1)
262
if name not in api.Command:
263
print "Command %s in API file, not in ipalib" % name
264
rval |= API_FILE_DIFFERENCE
267
existing_cmds.append(name)
268
cmd = api.Command[name]
272
if line.startswith('args:') and cmd:
273
line = line.replace('args: ', '')
274
(expected_args, expected_options, expected_output) = line.split(',')
275
expected_args = int(expected_args)
276
expected_options = int(expected_options)
277
expected_output = int(expected_output)
278
if line.startswith('arg:') and cmd:
279
line = line.replace('arg: ', '')
281
arg = find_name(line)
283
if strip_doc(repr(a)) == line:
288
print 'Arg in %s doesn\'t match.\nGot %s\nExpected %s' % (
289
name, strip_doc(repr(a)), line)
290
rval |= API_FILE_DIFFERENCE
292
found_args.append(arg)
294
arg = find_name(line)
295
print "Argument '%s' in command '%s' in API file not found" % (arg, name)
296
rval |= API_FILE_DIFFERENCE
297
if line.startswith('option:') and cmd:
298
line = line.replace('option: ', '')
300
option = find_name(line)
301
for o in cmd.options():
302
if strip_doc(repr(o)) == line:
307
print 'Option in %s doesn\'t match. Got %s Expected %s' % (name, o, line)
308
rval |= API_FILE_DIFFERENCE
310
found_options.append(option)
312
option = find_name(line)
313
print "Option '%s' in command '%s' in API file not found" % (option, name)
314
rval |= API_FILE_DIFFERENCE
315
if line.startswith('output:') and cmd:
316
line = line.replace('output: ', '')
318
output = find_name(line)
319
for o in cmd.output():
320
if strip_doc(repr(o)) == line:
325
print 'Output in %s doesn\'t match. Got %s Expected %s' % (name, o, line)
326
rval |= API_FILE_DIFFERENCE
328
found_output.append(output)
330
output = find_name(line)
331
print "Option '%s' in command '%s' in API file not found" % (output, name)
332
rval |= API_FILE_DIFFERENCE
335
if not _finalize_command_validation(cmd, found_args, expected_args,
336
found_options, expected_options,
337
found_output, expected_output):
338
rval |= API_FILE_DIFFERENCE
340
# Now look for new commands not in the current API
341
for cmd in api.Command():
342
if cmd.name not in existing_cmds:
343
print "Command %s in ipalib, not in API" % cmd.name
344
rval |= API_NEW_COMMAND
350
options, args = parse_options()
365
if options.validate_doc:
366
rval |= validate_doc()
369
if not os.path.exists(API_FILE):
370
print 'No %s to validate' % API_FILE
373
rval |= validate_api()
375
print "Writing API to API.txt"
378
if rval & API_FILE_DIFFERENCE:
380
print 'There are one or more changes to the API.\nEither undo the API changes or update API.txt and increment the major version in VERSION.'
382
if rval & API_NEW_COMMAND:
384
print 'There are one or more new commands defined.\nUpdate API.txt and increment the minor version in VERSION.'
386
if rval & API_DOC_ERROR:
388
print 'There are one or more documentation problems.\nYou must fix these before preceeding'