fbe783b7fab3b904f6f5975eb89e502f4d10a6c4
[kai/samba.git] / python / samba / tests / docs.py
1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2012
3 #
4 # Tests for documentation.
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 3 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,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU 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, see <http://www.gnu.org/licenses/>.
18 #
19
20 """Tests for presence of documentation."""
21
22 import samba
23 import samba.tests
24
25 import os
26 import re
27 import subprocess
28 import xml.etree.ElementTree as ET
29
30 class TestCase(samba.tests.TestCaseInTempDir):
31
32     def _format_message(self, parameters, message):
33         parameters = list(parameters)
34         parameters = map(str, parameters)
35         parameters.sort()
36         return message + '\n\n    %s' % ('\n    '.join(parameters))
37
38
39 def get_documented_parameters(sourcedir):
40     path = os.path.join(sourcedir, "bin", "default", "docs-xml", "smbdotconf")
41     if not os.path.exists(os.path.join(path, "parameters.all.xml")):
42         raise Exception("Unable to find parameters.all.xml")
43     try:
44         p = open(os.path.join(path, "parameters.all.xml"), 'r')
45     except IOError, e:
46         raise Exception("Error opening parameters file")
47     out = p.read()
48
49     root = ET.fromstring(out)
50     for parameter in root:
51         name = parameter.attrib.get('name')
52         if parameter.attrib.get('removed') == "1":
53            continue
54         yield name
55         syn = parameter.findall('synonym')
56         if syn is not None:
57             for sy in syn:
58                 yield sy.text
59     p.close()
60
61
62 def get_implementation_parameters(sourcedir):
63     # Reading entries from source code
64     f = open(os.path.join(sourcedir, "lib/param/param_table_static.c"), "r")
65     try:
66         # burn through the preceding lines
67         while True:
68             l = f.readline()
69             if l.startswith("struct parm_struct parm_table"):
70                 break
71
72         for l in f.readlines():
73             if re.match("^\s*\}\;\s*$", l):
74                 break
75             # pull in the param names only
76             m = re.match("\s*\.label\s*=\s*\"(.*)\".*", l)
77             if not m:
78                 continue
79
80             name = m.group(1)
81             yield name
82     finally:
83         f.close()
84
85 def get_param_table_full(sourcedir, filename="lib/param/param_table_static.c"):
86     # Reading entries from source code
87     f = open(os.path.join(sourcedir, filename), "r")
88     try:
89         # burn through the preceding lines
90         while True:
91             l = f.readline()
92             if l.startswith("struct parm_struct parm_table"):
93                 break
94
95         for l in f.readlines():
96
97             if re.match("^\s*\}\;\s*$", l):
98                 # end of the table reached
99                 break
100
101             if re.match("^\s*\{\s*$", l):
102                 # start a new entry
103                 _label = ""
104                 _type = ""
105                 _class = ""
106                 _offset = ""
107                 _special = ""
108                 _enum_list = ""
109                 _flags = ""
110                 continue
111
112             if re.match("^\s*\},\s*$", l):
113                 # finish the entry
114                 yield _label, _type, _class, _offset, _special, _enum_list, _flags
115                 continue
116
117             m = re.match("^\s*\.([^\s]+)\s*=\s*(.*),.*", l)
118             if not m:
119                 continue
120
121             attrib = m.group(1)
122             value = m.group(2)
123
124             if attrib == "label":
125                 _label = value
126             elif attrib == "type":
127                 _type = value
128             elif attrib == "p_class":
129                 _class = value
130             elif attrib == "offset":
131                 _offset = value
132             elif attrib == "special":
133                 _special = value
134             elif attrib == "enum_list":
135                 _special = value
136             elif attrib == "flags":
137                 _flags = value
138
139     finally:
140         f.close()
141
142
143 def get_documented_tuples(sourcedir, omit_no_default=True):
144     path = os.path.join(sourcedir, "bin", "default", "docs-xml", "smbdotconf")
145     if not os.path.exists(os.path.join(path, "parameters.all.xml")):
146         raise Exception("Unable to find parameters.all.xml")
147     try:
148         p = open(os.path.join(path, "parameters.all.xml"), 'r')
149     except IOError, e:
150         raise Exception("Error opening parameters file")
151     out = p.read()
152
153     root = ET.fromstring(out)
154     for parameter in root:
155         name = parameter.attrib.get("name")
156         param_type = parameter.attrib.get("type")
157         if parameter.attrib.get('removed') == "1":
158            continue
159         values = parameter.findall("value")
160         defaults = []
161         for value in values:
162             if value.attrib.get("type") == "default":
163                 defaults.append(value)
164
165         default_text = None
166         if len(defaults) == 0:
167             if omit_no_default:
168                 continue
169         elif len(defaults) > 1:
170             raise Exception("More than one default found for parameter %s" % name)
171         else:
172             default_text = defaults[0].text
173
174         if default_text is None:
175             default_text = ""
176         context = parameter.attrib.get("context")
177         yield name, default_text, context, param_type
178     p.close()
179
180 class SmbDotConfTests(TestCase):
181
182     # defines the cases where the defaults may differ from the documentation
183     special_cases = set(['log level', 'path', 'ldapsam:trusted', 'spoolss: architecture',
184                          'share:fake_fscaps', 'ldapsam:editposix', 'rpc_daemon:DAEMON',
185                          'rpc_server:SERVER', 'panic action', 'homedir map', 'NIS homedir',
186                          'server string', 'netbios name', 'socket options', 'use mmap',
187                          'ctdbd socket', 'printing', 'printcap name', 'queueresume command',
188                          'queuepause command','lpresume command', 'lppause command',
189                          'lprm command', 'lpq command', 'print command', 'template homedir',
190                          'spoolss: os_major', 'spoolss: os_minor', 'spoolss: os_build',
191                          'max open files', 'fss: prune stale', 'fss: sequence timeout'])
192
193     def setUp(self):
194         super(SmbDotConfTests, self).setUp()
195         # create a minimal smb.conf file for testparm
196         self.smbconf = os.path.join(self.tempdir, "paramtestsmb.conf")
197         f = open(self.smbconf, 'w')
198         try:
199             f.write("""
200 [test]
201    path = /
202 """)
203         finally:
204             f.close()
205
206         self.blankconf = os.path.join(self.tempdir, "emptytestsmb.conf")
207         f = open(self.blankconf, 'w')
208         try:
209             f.write("")
210         finally:
211             f.close()
212
213         self.topdir = os.path.abspath(samba.source_tree_topdir())
214
215         try:
216             self.documented = set(get_documented_parameters(self.topdir))
217         except:
218             self.fail("Unable to load documented parameters")
219
220         try:
221             self.parameters = set(get_implementation_parameters(self.topdir))
222         except:
223             self.fail("Unable to load implemented parameters")
224
225         try:
226             self.table_static = set(get_param_table_full(self.topdir,
227                                    "lib/param/param_table_static.c"))
228         except:
229             self.fail("Unable to load static parameter table")
230
231         try:
232             self.defaults = set(get_documented_tuples(self.topdir))
233         except:
234             self.fail("Unable to load parameters")
235
236         try:
237             self.defaults_all = set(get_documented_tuples(self.topdir, False))
238         except:
239             self.fail("Unable to load parameters")
240
241
242     def tearDown(self):
243         super(SmbDotConfTests, self).tearDown()
244         os.unlink(self.smbconf)
245         os.unlink(self.blankconf)
246
247     def test_unknown(self):
248         # Filter out parametric options, since we can't find them in the parm
249         # table
250         documented = set([p for p in self.documented if not ":" in p])
251         unknown = documented.difference(self.parameters)
252         if len(unknown) > 0:
253             self.fail(self._format_message(unknown,
254                 "Parameters that are documented but not in the implementation:"))
255
256     def test_undocumented(self):
257         undocumented = self.parameters.difference(self.documented)
258         if len(undocumented) > 0:
259             self.fail(self._format_message(undocumented,
260                 "Parameters that are in the implementation but undocumented:"))
261
262     def test_default_s3(self):
263         self._test_default(['bin/testparm'])
264         self._set_defaults(['bin/testparm'])
265
266         # registry shares appears to need sudo
267         self._set_arbitrary(['bin/testparm'],
268             exceptions = ['client lanman auth',
269                           'client plaintext auth',
270                           'registry shares',
271                           'smb ports'])
272         self._test_empty(['bin/testparm'])
273
274     def test_default_s4(self):
275         self._test_default(['bin/samba-tool', 'testparm'])
276         self._set_defaults(['bin/samba-tool', 'testparm'])
277         self._set_arbitrary(['bin/samba-tool', 'testparm'],
278             exceptions = ['smb ports'])
279         self._test_empty(['bin/samba-tool', 'testparm'])
280
281     def _test_default(self, program):
282         failset = set()
283         count = 0
284
285         for tuples in self.defaults:
286             param, default, context, param_type = tuples
287             if param in self.special_cases:
288                 continue
289             section = None
290             if context == "G":
291                 section = "global"
292             elif context == "S":
293                 section = "test"
294             else:
295                  self.fail("%s has no valid context" % param)
296             p = subprocess.Popen(program + ["-s", self.smbconf,
297                     "--section-name", section, "--parameter-name", param],
298                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
299             if p[0].upper().strip() != default.upper():
300                 if not (p[0].upper().strip() == "" and default == '""'):
301                     doc_triple = "%s\n      Expected: %s" % (param, default)
302                     failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
303
304         if len(failset) > 0:
305             self.fail(self._format_message(failset,
306                 "Parameters that do not have matching defaults:"))
307
308     def _set_defaults(self, program):
309         failset = set()
310         count = 0
311
312         for tuples in self.defaults:
313             param, default, context, param_type = tuples
314
315             if param in ['printing']:
316                 continue
317
318             section = None
319             if context == "G":
320                 section = "global"
321             elif context == "S":
322                 section = "test"
323             else:
324                  self.fail("%s has no valid context" % param)
325             p = subprocess.Popen(program + ["-s", self.smbconf,
326                     "--section-name", section, "--parameter-name", param,
327                     "--option", "%s = %s" % (param, default)],
328                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
329             if p[0].upper().strip() != default.upper():
330                 if not (p[0].upper().strip() == "" and default == '""'):
331                     doc_triple = "%s\n      Expected: %s" % (param, default)
332                     failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
333
334         if len(failset) > 0:
335             self.fail(self._format_message(failset,
336                 "Parameters that do not have matching defaults:"))
337
338     def _set_arbitrary(self, program, exceptions=None):
339         arbitrary = {'string': 'string', 'boolean': 'yes', 'integer': '5',
340                      'boolean-rev': 'yes',
341                      'cmdlist': 'a b c',
342                      'bytes': '10',
343                      'octal': '0123',
344                      'ustring': 'ustring',
345                      'enum':'', 'boolean-auto': '', 'char': 'a', 'list': 'a, b, c'}
346         opposite_arbitrary = {'string': 'string2', 'boolean': 'no', 'integer': '6',
347                               'boolean-rev': 'no',
348                               'cmdlist': 'd e f',
349                               'bytes': '11',
350                               'octal': '0567',
351                               'ustring': 'ustring2',
352                               'enum':'', 'boolean-auto': '', 'char': 'b', 'list': 'd, e, f'}
353
354         failset = set()
355         count = 0
356
357         for tuples in self.defaults_all:
358             param, default, context, param_type = tuples
359
360             if param in ['printing', 'copy', 'include', 'log level']:
361                 continue
362
363             # currently no easy way to set an arbitrary value for these
364             if param_type in ['enum', 'boolean-auto']:
365                 continue
366
367             if exceptions is not None:
368                 if param in exceptions:
369                     continue
370
371             section = None
372             if context == "G":
373                 section = "global"
374             elif context == "S":
375                 section = "test"
376             else:
377                  self.fail("%s has no valid context" % param)
378
379             value_to_use = arbitrary.get(param_type)
380             if value_to_use is None:
381                 self.fail("%s has an invalid type" % param)
382
383             p = subprocess.Popen(program + ["-s", self.smbconf,
384                     "--section-name", section, "--parameter-name", param,
385                     "--option", "%s = %s" % (param, value_to_use)],
386                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
387             if p[0].upper().strip() != value_to_use.upper():
388                 # currently no way to distinguish command lists
389                 if param_type == 'list':
390                     if ", ".join(p[0].upper().strip().split()) == value_to_use.upper():
391                         continue
392
393                 # currently no way to identify octal
394                 if param_type == 'integer':
395                     try:
396                         if int(value_to_use, 8) == int(p[0].strip(), 8):
397                             continue
398                     except:
399                         pass
400
401                 doc_triple = "%s\n      Expected: %s" % (param, value_to_use)
402                 failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
403
404             opposite_value = opposite_arbitrary.get(param_type)
405             tempconf = os.path.join(self.tempdir, "tempsmb.conf")
406             g = open(tempconf, 'w')
407             try:
408                 towrite = section + "\n"
409                 towrite += param + " = " + opposite_value
410                 g.write(towrite)
411             finally:
412                 g.close()
413
414             p = subprocess.Popen(program + ["-s", tempconf, "--suppress-prompt",
415                     "--option", "%s = %s" % (param, value_to_use)],
416                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
417
418             os.unlink(tempconf)
419
420             # testparm doesn't display a value if they are equivalent
421             if (value_to_use.lower() != opposite_value.lower()):
422                 for line in p[0].splitlines():
423                     if not line.strip().startswith(param):
424                         continue
425
426                     value_found = line.split("=")[1].upper().strip()
427                     if value_found != value_to_use.upper():
428                         # currently no way to distinguish command lists
429                         if param_type == 'list':
430                             if ", ".join(value_found.split()) == value_to_use.upper():
431                                 continue
432
433                         # currently no way to identify octal
434                         if param_type == 'integer':
435                             try:
436                                 if int(value_to_use, 8) == int(value_found, 8):
437                                     continue
438                             except:
439                                 pass
440
441                         doc_triple = "%s\n      Expected: %s" % (param, value_to_use)
442                         failset.add("%s\n      Got: %s" % (doc_triple, value_found))
443
444
445         if len(failset) > 0:
446             self.fail(self._format_message(failset,
447                 "Parameters that were unexpectedly not set:"))
448
449     def _test_empty(self, program):
450         p = subprocess.Popen(program + ["-s", self.blankconf, "--suppress-prompt"],
451                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
452         output = ""
453
454         for line in p[0].splitlines():
455             if line.strip().startswith('#'):
456                 continue
457             if line.strip().startswith("idmap config *"):
458                 continue
459             output += line.strip().lower() + '\n'
460
461         if output.strip() != '[global]' and output.strip() != '[globals]':
462             self.fail("Testparm returned unexpected output on an empty smb.conf.")