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