c75f18cb43b36ef8d0a46d426f97c5985a6bb1cc
[bbaumbach/samba-autobuild/.git] / python / samba / tests / usage.py
1 # Unix SMB/CIFS implementation.
2 # Copyright © Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import os
18 import subprocess
19 from samba.tests import TestCase, check_help_consistency
20 import re
21 import stat
22
23 if 'SRCDIR_ABS' in os.environ:
24     BASEDIR = os.environ['SRCDIR_ABS']
25 else:
26     BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
27                                            '../../..'))
28
29 TEST_DIRS = [
30     "bootstrap",
31     "testdata",
32     "ctdb",
33     "dfs_server",
34     "pidl",
35     "auth",
36     "packaging",
37     "python",
38     "include",
39     "nsswitch",
40     "libcli",
41     "coverity",
42     "release-scripts",
43     "testprogs",
44     "bin",
45     "source3",
46     "docs-xml",
47     "buildtools",
48     "file_server",
49     "dynconfig",
50     "source4",
51     "tests",
52     "libds",
53     "selftest",
54     "lib",
55     "script",
56     "traffic",
57     "testsuite",
58     "libgpo",
59     "wintest",
60     "librpc",
61 ]
62
63
64 EXCLUDE_USAGE = {
65     'script/autobuild.py',  # defaults to mount /memdisk/
66     'script/bisect-test.py',
67     'ctdb/utils/etcd/ctdb_etcd_lock',
68     'selftest/filter-subunit',
69     'selftest/format-subunit',
70     'bin/gen_output.py',  # too much output!
71     'source4/scripting/bin/gen_output.py',
72     'lib/ldb/tests/python/index.py',
73     'lib/ldb/tests/python/api.py',
74     'source4/selftest/tests.py',
75     'buildtools/bin/waf',
76     'selftest/tap2subunit',
77     'script/show_test_time',
78     'source4/scripting/bin/subunitrun',
79     'bin/samba_downgrade_db',
80     'source4/scripting/bin/samba_downgrade_db',
81     'source3/selftest/tests.py',
82     'selftest/tests.py',
83     'python/samba/subunit/run.py',
84     'bin/python/samba/subunit/run.py',
85     'lib/compression/tests/scripts/three-byte-hash',
86 }
87
88 EXCLUDE_HELP = {
89     'selftest/tap2subunit',
90     'wintest/test-s3.py',
91     'wintest/test-s4-howto.py',
92 }
93
94
95 EXCLUDE_DIRS = {
96     'source3/script/tests',
97     'python/examples',
98     'source4/dsdb/tests/python',
99     'bin/ab',
100     'bin/python/samba/tests',
101     'bin/python/samba/tests/dcerpc',
102     'bin/python/samba/tests/krb5',
103     'python/samba/tests',
104     'python/samba/tests/bin',
105     'python/samba/tests/dcerpc',
106     'python/samba/tests/krb5',
107 }
108
109
110 def _init_git_file_finder():
111     """Generate a function that quickly answers the question:
112     'is this a git file?'
113     """
114     git_file_cache = set()
115     p = subprocess.run(['git',
116                         '-C', BASEDIR,
117                         'ls-files',
118                         '-z'],
119                        stdout=subprocess.PIPE)
120     if p.returncode == 0:
121         for fn in p.stdout.split(b'\0'):
122             git_file_cache.add(os.path.join(BASEDIR, fn.decode('utf-8')))
123     return git_file_cache.__contains__
124
125
126 is_git_file = _init_git_file_finder()
127
128
129 def script_iterator(d=BASEDIR, cache=None,
130                     shebang_filter=None,
131                     filename_filter=None,
132                     subdirs=None):
133     if subdirs is None:
134         subdirs = TEST_DIRS
135     if not cache:
136         safename = re.compile(r'\W+').sub
137         for subdir in subdirs:
138             sd = os.path.join(d, subdir)
139             for root, dirs, files in os.walk(sd, followlinks=False):
140                 for fn in files:
141                     if fn.endswith('~'):
142                         continue
143                     if fn.endswith('.inst'):
144                         continue
145                     ffn = os.path.join(root, fn)
146                     try:
147                         s = os.stat(ffn)
148                     except FileNotFoundError:
149                         continue
150                     if not s.st_mode & stat.S_IXUSR:
151                         continue
152                     if not (subdir == 'bin' or is_git_file(ffn)):
153                         continue
154
155                     if filename_filter is not None:
156                         if not filename_filter(ffn):
157                             continue
158
159                     if shebang_filter is not None:
160                         try:
161                             f = open(ffn, 'rb')
162                         except OSError as e:
163                             print("could not open %s: %s" % (ffn, e))
164                             continue
165                         line = f.read(40)
166                         f.close()
167                         if not shebang_filter(line):
168                             continue
169
170                     name = safename('_', fn)
171                     while name in cache:
172                         name += '_'
173                     cache[name] = ffn
174
175     return cache.items()
176
177 # For ELF we only look at /bin/* top level.
178 def elf_file_name(fn):
179     fn = fn.partition('bin/')[2]
180     return fn and '/' not in fn and 'test' not in fn and 'ldb' in fn
181
182 def elf_shebang(x):
183     return x[:4] == b'\x7fELF'
184
185 elf_cache = {}
186 def elf_iterator():
187     return script_iterator(BASEDIR, elf_cache,
188                            shebang_filter=elf_shebang,
189                            filename_filter=elf_file_name,
190                            subdirs=['bin'])
191
192
193 perl_shebang = re.compile(br'#!.+perl').match
194
195 perl_script_cache = {}
196 def perl_script_iterator():
197     return script_iterator(BASEDIR, perl_script_cache, perl_shebang)
198
199
200 python_shebang = re.compile(br'#!.+python').match
201
202 python_script_cache = {}
203 def python_script_iterator():
204     return script_iterator(BASEDIR, python_script_cache, python_shebang)
205
206
207 class PerlScriptUsageTests(TestCase):
208     """Perl scripts run without arguments should print a usage string,
209         not fail with a traceback.
210     """
211
212     @classmethod
213     def initialise(cls):
214         for name, filename in perl_script_iterator():
215             print(name, filename)
216
217
218 class PythonScriptUsageTests(TestCase):
219     """Python scripts run without arguments should print a usage string,
220         not fail with a traceback.
221     """
222
223     @classmethod
224     def initialise(cls):
225         for name, filename in python_script_iterator():
226             # We add the actual tests after the class definition so we
227             # can give individual names to them, so we can have a
228             # knownfail list.
229             fn = filename.replace(BASEDIR, '').lstrip('/')
230
231             if fn in EXCLUDE_USAGE:
232                 print("skipping %s (EXCLUDE_USAGE)" % filename)
233                 continue
234
235             if os.path.dirname(fn) in EXCLUDE_DIRS:
236                 print("skipping %s (EXCLUDE_DIRS)" % filename)
237                 continue
238
239             def _f(self, filename=filename):
240                 print(filename)
241                 try:
242                     p = subprocess.Popen(['python3', filename],
243                                          stderr=subprocess.PIPE,
244                                          stdout=subprocess.PIPE)
245                     out, err = p.communicate(timeout=5)
246                 except OSError as e:
247                     self.fail("Error: %s" % e)
248                 except subprocess.SubprocessError as e:
249                     self.fail("Subprocess error: %s" % e)
250
251                 err = err.decode('utf-8')
252                 out = out.decode('utf-8')
253                 self.assertNotIn('Traceback', err)
254
255                 self.assertIn('usage', out.lower() + err.lower(),
256                               'stdout:\n%s\nstderr:\n%s' % (out, err))
257
258             setattr(cls, 'test_%s' % name, _f)
259
260
261 class HelpTestSuper(TestCase):
262     """Python scripts run with -h or --help should print a help string,
263     and exit with success.
264     """
265     check_return_code = True
266     check_consistency = True
267     check_contains_usage = True
268     check_multiline = True
269     check_merged_out_and_err = False
270
271     interpreter = None
272
273     options_start = None
274     options_end = None
275     def iterator(self):
276         raise NotImplementedError("Subclass this "
277                                   "and add an iterator function!")
278
279     @classmethod
280     def initialise(cls):
281         for name, filename in cls.iterator():
282             # We add the actual tests after the class definition so we
283             # can give individual names to them, so we can have a
284             # knownfail list.
285             fn = filename.replace(BASEDIR, '').lstrip('/')
286
287             if fn in EXCLUDE_HELP:
288                 print("skipping %s (EXCLUDE_HELP)" % filename)
289                 continue
290
291             if os.path.dirname(fn) in EXCLUDE_DIRS:
292                 print("skipping %s (EXCLUDE_DIRS)" % filename)
293                 continue
294
295             def _f(self, filename=filename):
296                 print(filename)
297                 for h in ('--help', '-h'):
298                     cmd = [filename, h]
299                     if self.interpreter:
300                         cmd.insert(0, self.interpreter)
301                     try:
302                         p = subprocess.Popen(cmd,
303                                              stderr=subprocess.PIPE,
304                                              stdout=subprocess.PIPE)
305                         out, err = p.communicate(timeout=5)
306                     except OSError as e:
307                         self.fail("Error: %s" % e)
308                     except subprocess.SubprocessError as e:
309                         self.fail("Subprocess error: %s" % e)
310
311                     err = err.decode('utf-8')
312                     out = out.decode('utf-8')
313                     if self.check_merged_out_and_err:
314                         out = "%s\n%s" % (out, err)
315
316                     outl = out[:500].lower()
317                     # NOTE:
318                     # These assertions are heuristics, not policy.
319                     # If your script fails this test when it shouldn't
320                     # just add it to EXCLUDE_HELP above or change the
321                     # heuristic.
322
323                     # --help should produce:
324                     #    * multiple lines of help on stdout (not stderr),
325                     #    * including a "Usage:" string,
326                     #    * not contradict itself or repeat options,
327                     #    * and return success.
328                     #print(out.encode('utf8'))
329                     #print(err.encode('utf8'))
330                     if self.check_consistency:
331                         errors = check_help_consistency(out,
332                                                         self.options_start,
333                                                         self.options_end)
334                         if errors is not None:
335                             self.fail(errors)
336
337                     if self.check_return_code:
338                         self.assertEqual(p.returncode, 0,
339                                          "%s %s\nreturncode should not be %d\n"
340                                          "err:\n%s\nout:\n%s" %
341                                          (filename, h, p.returncode, err, out))
342                     if self.check_contains_usage:
343                         self.assertIn('usage', outl, 'lacks "Usage:"\n')
344                     if self.check_multiline:
345                         self.assertIn('\n', out, 'expected multi-line output')
346
347             setattr(cls, 'test_%s' % name, _f)
348
349
350 class PythonScriptHelpTests(HelpTestSuper):
351     """Python scripts run with -h or --help should print a help string,
352     and exit with success.
353     """
354     iterator = python_script_iterator
355     interpreter = 'python3'
356
357
358 class ElfHelpTests(HelpTestSuper):
359     """ELF binaries run with -h or --help should print a help string,
360     and exit with success.
361     """
362     iterator = elf_iterator
363     check_return_code = False
364     check_merged_out_and_err = True
365
366
367 PerlScriptUsageTests.initialise()
368 PythonScriptUsageTests.initialise()
369 PythonScriptHelpTests.initialise()
370 ElfHelpTests.initialise()