tests:docs: remove testing the diff between the static and generated table
[amitay/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.table_gen = set(get_param_table_full(self.topdir,
233                                  "bin/default/lib/param/param_table_gen.c"))
234         except:
235             self.fail("Unable to load generated parameter table")
236
237         try:
238             self.defaults = set(get_documented_tuples(self.topdir))
239         except:
240             self.fail("Unable to load parameters")
241
242         try:
243             self.defaults_all = set(get_documented_tuples(self.topdir, False))
244         except:
245             self.fail("Unable to load parameters")
246
247
248     def tearDown(self):
249         super(SmbDotConfTests, self).tearDown()
250         os.unlink(self.smbconf)
251         os.unlink(self.blankconf)
252
253     def test_unknown(self):
254         # Filter out parametric options, since we can't find them in the parm
255         # table
256         documented = set([p for p in self.documented if not ":" in p])
257         unknown = documented.difference(self.parameters)
258         if len(unknown) > 0:
259             self.fail(self._format_message(unknown,
260                 "Parameters that are documented but not in the implementation:"))
261
262     def test_undocumented(self):
263         undocumented = self.parameters.difference(self.documented)
264         if len(undocumented) > 0:
265             self.fail(self._format_message(undocumented,
266                 "Parameters that are in the implementation but undocumented:"))
267
268     def test_default_s3(self):
269         self._test_default(['bin/testparm'])
270         self._set_defaults(['bin/testparm'])
271
272         # registry shares appears to need sudo
273         self._set_arbitrary(['bin/testparm'],
274             exceptions = ['client lanman auth',
275                           'client plaintext auth',
276                           'registry shares',
277                           'smb ports'])
278         self._test_empty(['bin/testparm'])
279
280     def test_default_s4(self):
281         self._test_default(['bin/samba-tool', 'testparm'])
282         self._set_defaults(['bin/samba-tool', 'testparm'])
283         self._set_arbitrary(['bin/samba-tool', 'testparm'],
284             exceptions = ['smb ports'])
285         self._test_empty(['bin/samba-tool', 'testparm'])
286
287     def _test_default(self, program):
288         failset = set()
289         count = 0
290
291         for tuples in self.defaults:
292             param, default, context, param_type = tuples
293             if param in self.special_cases:
294                 continue
295             section = None
296             if context == "G":
297                 section = "global"
298             elif context == "S":
299                 section = "test"
300             else:
301                  self.fail("%s has no valid context" % param)
302             p = subprocess.Popen(program + ["-s", self.smbconf,
303                     "--section-name", section, "--parameter-name", param],
304                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
305             if p[0].upper().strip() != default.upper():
306                 if not (p[0].upper().strip() == "" and default == '""'):
307                     doc_triple = "%s\n      Expected: %s" % (param, default)
308                     failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
309
310         if len(failset) > 0:
311             self.fail(self._format_message(failset,
312                 "Parameters that do not have matching defaults:"))
313
314     def _set_defaults(self, program):
315         failset = set()
316         count = 0
317
318         for tuples in self.defaults:
319             param, default, context, param_type = tuples
320
321             if param in ['printing']:
322                 continue
323
324             section = None
325             if context == "G":
326                 section = "global"
327             elif context == "S":
328                 section = "test"
329             else:
330                  self.fail("%s has no valid context" % param)
331             p = subprocess.Popen(program + ["-s", self.smbconf,
332                     "--section-name", section, "--parameter-name", param,
333                     "--option", "%s = %s" % (param, default)],
334                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
335             if p[0].upper().strip() != default.upper():
336                 if not (p[0].upper().strip() == "" and default == '""'):
337                     doc_triple = "%s\n      Expected: %s" % (param, default)
338                     failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
339
340         if len(failset) > 0:
341             self.fail(self._format_message(failset,
342                 "Parameters that do not have matching defaults:"))
343
344     def _set_arbitrary(self, program, exceptions=None):
345         arbitrary = {'string': 'string', 'boolean': 'yes', 'integer': '5',
346                      'boolean-rev': 'yes',
347                      'cmdlist': 'a b c',
348                      'bytes': '10',
349                      'octal': '0123',
350                      'ustring': 'ustring',
351                      'enum':'', 'boolean-auto': '', 'char': 'a', 'list': 'a, b, c'}
352         opposite_arbitrary = {'string': 'string2', 'boolean': 'no', 'integer': '6',
353                               'boolean-rev': 'no',
354                               'cmdlist': 'd e f',
355                               'bytes': '11',
356                               'octal': '0567',
357                               'ustring': 'ustring2',
358                               'enum':'', 'boolean-auto': '', 'char': 'b', 'list': 'd, e, f'}
359
360         failset = set()
361         count = 0
362
363         for tuples in self.defaults_all:
364             param, default, context, param_type = tuples
365
366             if param in ['printing', 'copy', 'include', 'log level']:
367                 continue
368
369             # currently no easy way to set an arbitrary value for these
370             if param_type in ['enum', 'boolean-auto']:
371                 continue
372
373             if exceptions is not None:
374                 if param in exceptions:
375                     continue
376
377             section = None
378             if context == "G":
379                 section = "global"
380             elif context == "S":
381                 section = "test"
382             else:
383                  self.fail("%s has no valid context" % param)
384
385             value_to_use = arbitrary.get(param_type)
386             if value_to_use is None:
387                 self.fail("%s has an invalid type" % param)
388
389             p = subprocess.Popen(program + ["-s", self.smbconf,
390                     "--section-name", section, "--parameter-name", param,
391                     "--option", "%s = %s" % (param, value_to_use)],
392                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
393             if p[0].upper().strip() != value_to_use.upper():
394                 # currently no way to distinguish command lists
395                 if param_type == 'list':
396                     if ", ".join(p[0].upper().strip().split()) == value_to_use.upper():
397                         continue
398
399                 # currently no way to identify octal
400                 if param_type == 'integer':
401                     try:
402                         if int(value_to_use, 8) == int(p[0].strip(), 8):
403                             continue
404                     except:
405                         pass
406
407                 doc_triple = "%s\n      Expected: %s" % (param, value_to_use)
408                 failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
409
410             opposite_value = opposite_arbitrary.get(param_type)
411             tempconf = os.path.join(self.tempdir, "tempsmb.conf")
412             g = open(tempconf, 'w')
413             try:
414                 towrite = section + "\n"
415                 towrite += param + " = " + opposite_value
416                 g.write(towrite)
417             finally:
418                 g.close()
419
420             p = subprocess.Popen(program + ["-s", tempconf, "--suppress-prompt",
421                     "--option", "%s = %s" % (param, value_to_use)],
422                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
423
424             os.unlink(tempconf)
425
426             # testparm doesn't display a value if they are equivalent
427             if (value_to_use.lower() != opposite_value.lower()):
428                 for line in p[0].splitlines():
429                     if not line.strip().startswith(param):
430                         continue
431
432                     value_found = line.split("=")[1].upper().strip()
433                     if value_found != value_to_use.upper():
434                         # currently no way to distinguish command lists
435                         if param_type == 'list':
436                             if ", ".join(value_found.split()) == value_to_use.upper():
437                                 continue
438
439                         # currently no way to identify octal
440                         if param_type == 'integer':
441                             try:
442                                 if int(value_to_use, 8) == int(value_found, 8):
443                                     continue
444                             except:
445                                 pass
446
447                         doc_triple = "%s\n      Expected: %s" % (param, value_to_use)
448                         failset.add("%s\n      Got: %s" % (doc_triple, value_found))
449
450
451         if len(failset) > 0:
452             self.fail(self._format_message(failset,
453                 "Parameters that were unexpectedly not set:"))
454
455     def _test_empty(self, program):
456         p = subprocess.Popen(program + ["-s", self.blankconf, "--suppress-prompt"],
457                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.topdir).communicate()
458         output = ""
459
460         for line in p[0].splitlines():
461             if line.strip().startswith('#'):
462                 continue
463             if line.strip().startswith("idmap config *"):
464                 continue
465             output += line.strip().lower() + '\n'
466
467         if output.strip() != '[global]' and output.strip() != '[globals]':
468             self.fail("Testparm returned unexpected output on an empty smb.conf.")