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