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