f25df4368316c3f74d1cf165d56e0841c5c49423
[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 subprocess
27 import xml.etree.ElementTree as ET
28
29
30 class TestCase(samba.tests.TestCaseInTempDir):
31
32     def _format_message(self, parameters, message):
33         parameters = list(parameters)
34         parameters = list(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 as 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_documented_tuples(sourcedir, omit_no_default=True):
63     path = os.path.join(sourcedir, "bin", "default", "docs-xml", "smbdotconf")
64     if not os.path.exists(os.path.join(path, "parameters.all.xml")):
65         raise Exception("Unable to find parameters.all.xml")
66     try:
67         p = open(os.path.join(path, "parameters.all.xml"), 'r')
68     except IOError as e:
69         raise Exception("Error opening parameters file")
70     out = p.read()
71
72     root = ET.fromstring(out)
73     for parameter in root:
74         name = parameter.attrib.get("name")
75         param_type = parameter.attrib.get("type")
76         if parameter.attrib.get('removed') == "1":
77             continue
78         values = parameter.findall("value")
79         defaults = []
80         for value in values:
81             if value.attrib.get("type") == "default":
82                 defaults.append(value)
83
84         default_text = None
85         if len(defaults) == 0:
86             if omit_no_default:
87                 continue
88         elif len(defaults) > 1:
89             raise Exception("More than one default found for parameter %s" % name)
90         else:
91             default_text = defaults[0].text
92
93         if default_text is None:
94             default_text = ""
95         context = parameter.attrib.get("context")
96         yield name, default_text, context, param_type
97     p.close()
98
99
100 class SmbDotConfTests(TestCase):
101
102     # defines the cases where the defaults may differ from the documentation
103     special_cases = set([
104         'log level',
105         'path',
106         'panic action',
107         'homedir map',
108         'NIS homedir',
109         'server string',
110         'netbios name',
111         'socket options',
112         'use mmap',
113         'ctdbd socket',
114         'printing',
115         'printcap name',
116         'queueresume command',
117         'queuepause command',
118         'lpresume command',
119         'lppause command',
120         'lprm command',
121         'lpq command',
122         'print command',
123         'template homedir',
124         'max open files',
125         'include system krb5 conf',
126         'rpc server dynamic port range',
127         'mit kdc command',
128     ])
129
130     def setUp(self):
131         super(SmbDotConfTests, self).setUp()
132         # create a minimal smb.conf file for testparm
133         self.smbconf = os.path.join(self.tempdir, "paramtestsmb.conf")
134         f = open(self.smbconf, 'w')
135         try:
136             f.write("""
137 [test]
138    path = /
139 """)
140         finally:
141             f.close()
142
143         self.blankconf = os.path.join(self.tempdir, "emptytestsmb.conf")
144         f = open(self.blankconf, 'w')
145         try:
146             f.write("")
147         finally:
148             f.close()
149
150         self.topdir = os.path.abspath(samba.source_tree_topdir())
151
152         try:
153             self.documented = set(get_documented_parameters(self.topdir))
154         except:
155             self.fail("Unable to load documented parameters")
156
157         try:
158             self.defaults = set(get_documented_tuples(self.topdir))
159         except:
160             self.fail("Unable to load parameters")
161
162         try:
163             self.defaults_all = set(get_documented_tuples(self.topdir, False))
164         except:
165             self.fail("Unable to load parameters")
166
167     def tearDown(self):
168         super(SmbDotConfTests, self).tearDown()
169         os.unlink(self.smbconf)
170         os.unlink(self.blankconf)
171
172     def test_default_s3(self):
173         self._test_default(['bin/testparm'])
174         self._set_defaults(['bin/testparm'])
175
176         # registry shares appears to need sudo
177         self._set_arbitrary(['bin/testparm'],
178             exceptions = ['client lanman auth',
179                           'client plaintext auth',
180                           'registry shares',
181                           'smb ports',
182                           'rpc server dynamic port range',
183                           'name resolve order',
184                           'clustering'])
185         self._test_empty(['bin/testparm'])
186
187     def test_default_s4(self):
188         self._test_default(['bin/samba-tool', 'testparm'])
189         self._set_defaults(['bin/samba-tool', 'testparm'])
190         self._set_arbitrary(['bin/samba-tool', 'testparm'],
191                             exceptions=['smb ports',
192                                         'rpc server dynamic port range',
193                                         'name resolve order'])
194         self._test_empty(['bin/samba-tool', 'testparm'])
195
196     def _test_default(self, program):
197
198         if program[0] == 'bin/samba-tool' and os.getenv("PYTHON", None):
199             program = [os.environ["PYTHON"]] + program
200
201         failset = set()
202
203         for tuples in self.defaults:
204             param, default, context, param_type = tuples
205
206             if param in self.special_cases:
207                 continue
208             # bad, bad parametric options - we don't have their default values
209             if ':' in param:
210                 continue
211             section = None
212             if context == "G":
213                 section = "global"
214             elif context == "S":
215                 section = "test"
216             else:
217                 self.fail("%s has no valid context" % param)
218             p = subprocess.Popen(program + ["-s",
219                                             self.smbconf,
220                                             "--section-name",
221                                             section,
222                                             "--parameter-name",
223                                             param],
224                                  stdout=subprocess.PIPE,
225                                  stderr=subprocess.PIPE,
226                                  cwd=self.topdir).communicate()
227             result = p[0].decode().upper().strip()
228             if result != default.upper():
229                 if not (result == "" and default == '""'):
230                     doc_triple = "%s\n      Expected: %s" % (param, default)
231                     failset.add("%s\n      Got: %s" % (doc_triple, result))
232
233         if len(failset) > 0:
234             self.fail(self._format_message(failset,
235                                            "Parameters that do not have matching defaults:"))
236
237     def _set_defaults(self, program):
238
239         if program[0] == 'bin/samba-tool' and os.getenv("PYTHON", None):
240             program = [os.environ["PYTHON"]] + program
241
242         failset = set()
243
244         for tuples in self.defaults:
245             param, default, context, param_type = tuples
246
247             if param in ['printing', 'rpc server dynamic port range']:
248                 continue
249
250             section = None
251             if context == "G":
252                 section = "global"
253             elif context == "S":
254                 section = "test"
255             else:
256                 self.fail("%s has no valid context" % param)
257             p = subprocess.Popen(program + ["-s",
258                                             self.smbconf,
259                                             "--section-name",
260                                             section,
261                                             "--parameter-name",
262                                             param,
263                                             "--option",
264                                             "%s = %s" % (param, default)],
265                                  stdout=subprocess.PIPE,
266                                  stderr=subprocess.PIPE,
267                                  cwd=self.topdir).communicate()
268             result = p[0].decode().upper().strip()
269             if result != default.upper():
270                 if not (result == "" and default == '""'):
271                     doc_triple = "%s\n      Expected: %s" % (param, default)
272                     failset.add("%s\n      Got: %s" % (doc_triple, result))
273
274         if len(failset) > 0:
275             self.fail(self._format_message(failset,
276                                            "Parameters that do not have matching defaults:"))
277
278     def _set_arbitrary(self, program, exceptions=None):
279
280         if program[0] == 'bin/samba-tool' and os.getenv("PYTHON", None):
281             program = [os.environ["PYTHON"]] + program
282
283         arbitrary = {'string': 'string', 'boolean': 'yes', 'integer': '5',
284                      'boolean-rev': 'yes',
285                      'cmdlist': 'a b c',
286                      'bytes': '10',
287                      'octal': '0123',
288                      'ustring': 'ustring',
289                      'enum': '', 'boolean-auto': '', 'char': 'a', 'list': 'a, b, c'}
290         opposite_arbitrary = {'string': 'string2', 'boolean': 'no', 'integer': '6',
291                               'boolean-rev': 'no',
292                               'cmdlist': 'd e f',
293                               'bytes': '11',
294                               'octal': '0567',
295                               'ustring': 'ustring2',
296                               'enum': '', 'boolean-auto': '', 'char': 'b', 'list': 'd, e, f'}
297
298         failset = set()
299
300         for tuples in self.defaults_all:
301             param, default, context, param_type = tuples
302
303             if param in ['printing', 'copy', 'include', 'log level']:
304                 continue
305
306             # currently no easy way to set an arbitrary value for these
307             if param_type in ['enum', 'boolean-auto']:
308                 continue
309
310             if exceptions is not None:
311                 if param in exceptions:
312                     continue
313
314             section = None
315             if context == "G":
316                 section = "global"
317             elif context == "S":
318                 section = "test"
319             else:
320                 self.fail("%s has no valid context" % param)
321
322             value_to_use = arbitrary.get(param_type)
323             if value_to_use is None:
324                 self.fail("%s has an invalid type" % param)
325
326             p = subprocess.Popen(program + ["-s",
327                                             self.smbconf,
328                                             "--section-name",
329                                             section,
330                                             "--parameter-name",
331                                             param,
332                                             "--option",
333                                             "%s = %s" % (param, value_to_use)],
334                                  stdout=subprocess.PIPE,
335                                  stderr=subprocess.PIPE,
336                                  cwd=self.topdir).communicate()
337             result = p[0].decode().upper().strip()
338             if result != value_to_use.upper():
339                 # currently no way to distinguish command lists
340                 if param_type == 'list':
341                     if ", ".join(result.split()) == value_to_use.upper():
342                         continue
343
344                 # currently no way to identify octal
345                 if param_type == 'integer':
346                     try:
347                         if int(value_to_use, 8) == int(p[0].strip(), 8):
348                             continue
349                     except:
350                         pass
351
352                 doc_triple = "%s\n      Expected: %s" % (param, value_to_use)
353                 failset.add("%s\n      Got: %s" % (doc_triple, p[0].upper().strip()))
354
355             opposite_value = opposite_arbitrary.get(param_type)
356             tempconf = os.path.join(self.tempdir, "tempsmb.conf")
357             g = open(tempconf, 'w')
358             try:
359                 towrite = section + "\n"
360                 towrite += param + " = " + opposite_value
361                 g.write(towrite)
362             finally:
363                 g.close()
364
365             p = subprocess.Popen(program + ["-s",
366                                             tempconf,
367                                             "--suppress-prompt",
368                                             "--option",
369                                             "%s = %s" % (param, value_to_use)],
370                                  stdout=subprocess.PIPE,
371                                  stderr=subprocess.PIPE,
372                                  cwd=self.topdir).communicate()
373
374             os.unlink(tempconf)
375
376             # testparm doesn't display a value if they are equivalent
377             if (value_to_use.lower() != opposite_value.lower()):
378                 for line in p[0].decode().splitlines():
379                     if not line.strip().startswith(param):
380                         continue
381
382                     value_found = line.split("=")[1].upper().strip()
383                     if value_found != value_to_use.upper():
384                         # currently no way to distinguish command lists
385                         if param_type == 'list':
386                             if ", ".join(value_found.split()) == value_to_use.upper():
387                                 continue
388
389                         # currently no way to identify octal
390                         if param_type == 'integer':
391                             try:
392                                 if int(value_to_use, 8) == int(value_found, 8):
393                                     continue
394                             except:
395                                 pass
396
397                         doc_triple = "%s\n      Expected: %s" % (param, value_to_use)
398                         failset.add("%s\n      Got: %s" % (doc_triple, value_found))
399
400         if len(failset) > 0:
401             self.fail(self._format_message(failset,
402                                            "Parameters that were unexpectedly not set:"))
403
404     def _test_empty(self, program):
405
406         if program[0] == 'bin/samba-tool' and os.getenv("PYTHON", None):
407             program = [os.environ["PYTHON"]] + program
408
409         p = subprocess.Popen(program + ["-s",
410                                         self.blankconf,
411                                         "--suppress-prompt"],
412                              stdout=subprocess.PIPE,
413                              stderr=subprocess.PIPE,
414                              cwd=self.topdir).communicate()
415         output = ""
416
417         for line in p[0].decode().splitlines():
418             if line.strip().startswith('#'):
419                 continue
420             if line.strip().startswith("idmap config *"):
421                 continue
422             output += line.strip().lower() + '\n'
423
424         if output.strip() != '[global]' and output.strip() != '[globals]':
425             self.fail("Testparm returned unexpected output on an empty smb.conf.")