summaryrefslogtreecommitdiffstats
path: root/python/samba/tests/usage.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/samba/tests/usage.py')
-rw-r--r--python/samba/tests/usage.py380
1 files changed, 380 insertions, 0 deletions
diff --git a/python/samba/tests/usage.py b/python/samba/tests/usage.py
new file mode 100644
index 0000000..3312bfe
--- /dev/null
+++ b/python/samba/tests/usage.py
@@ -0,0 +1,380 @@
+# Unix SMB/CIFS implementation.
+# Copyright © Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import subprocess
+from samba.tests import TestCase, check_help_consistency
+import re
+import stat
+
+if 'SRCDIR_ABS' in os.environ:
+ BASEDIR = os.environ['SRCDIR_ABS']
+else:
+ BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '../../..'))
+
+TEST_DIRS = [
+ "bootstrap",
+ "testdata",
+ "ctdb",
+ "dfs_server",
+ "pidl",
+ "auth",
+ "packaging",
+ "python",
+ "include",
+ "nsswitch",
+ "libcli",
+ "coverity",
+ "release-scripts",
+ "testprogs",
+ "bin",
+ "source3",
+ "docs-xml",
+ "buildtools",
+ "file_server",
+ "dynconfig",
+ "source4",
+ "tests",
+ "libds",
+ "selftest",
+ "lib",
+ "script",
+ "traffic",
+ "testsuite",
+ "libgpo",
+ "wintest",
+ "librpc",
+]
+
+
+EXCLUDE_USAGE = {
+ 'script/autobuild.py', # defaults to mount /memdisk/
+ 'script/bisect-test.py',
+ 'ctdb/utils/etcd/ctdb_etcd_lock',
+ 'selftest/filter-subunit',
+ 'selftest/format-subunit',
+ 'bin/gen_output.py', # too much output!
+ 'source4/scripting/bin/gen_output.py',
+ 'lib/ldb/tests/python/index.py',
+ 'lib/ldb/tests/python/api.py',
+ 'source4/selftest/tests.py',
+ 'buildtools/bin/waf',
+ 'selftest/tap2subunit',
+ 'script/show_test_time',
+ 'source4/scripting/bin/subunitrun',
+ 'bin/samba_downgrade_db',
+ 'source4/scripting/bin/samba_downgrade_db',
+ 'source3/selftest/tests.py',
+ 'selftest/tests.py',
+ 'python/samba/subunit/run.py',
+ 'bin/python/samba/subunit/run.py',
+ 'lib/compression/tests/scripts/three-byte-hash',
+}
+
+EXCLUDE_HELP = {
+ 'selftest/tap2subunit',
+ 'wintest/test-s3.py',
+ 'wintest/test-s4-howto.py',
+}
+
+
+EXCLUDE_DIRS = {
+ 'source3/script/tests',
+ 'python/examples',
+ 'source4/dsdb/tests/python',
+ 'bin/ab',
+ 'bin/python/samba/tests',
+ 'bin/python/samba/tests/blackbox',
+ 'bin/python/samba/tests/dcerpc',
+ 'bin/python/samba/tests/krb5',
+ 'bin/python/samba/tests/ndr',
+ 'python/samba/tests',
+ 'python/samba/tests/bin',
+ 'python/samba/tests/blackbox',
+ 'python/samba/tests/dcerpc',
+ 'python/samba/tests/krb5',
+ 'python/samba/tests/ndr',
+}
+
+
+def _init_git_file_finder():
+ """Generate a function that quickly answers the question:
+ 'is this a git file?'
+ """
+ git_file_cache = set()
+ p = subprocess.run(['git',
+ '-C', BASEDIR,
+ 'ls-files',
+ '-z'],
+ stdout=subprocess.PIPE)
+ if p.returncode == 0:
+ for fn in p.stdout.split(b'\0'):
+ git_file_cache.add(os.path.join(BASEDIR, fn.decode('utf-8')))
+ return git_file_cache.__contains__
+
+
+is_git_file = _init_git_file_finder()
+
+
+def script_iterator(d=BASEDIR, cache=None,
+ shebang_filter=None,
+ filename_filter=None,
+ subdirs=None):
+ if subdirs is None:
+ subdirs = TEST_DIRS
+ if not cache:
+ safename = re.compile(r'\W+').sub
+ for subdir in subdirs:
+ sd = os.path.join(d, subdir)
+ for root, dirs, files in os.walk(sd, followlinks=False):
+ for fn in files:
+ if fn.endswith('~'):
+ continue
+ if fn.endswith('.inst'):
+ continue
+ ffn = os.path.join(root, fn)
+ try:
+ s = os.stat(ffn)
+ except FileNotFoundError:
+ continue
+ if not s.st_mode & stat.S_IXUSR:
+ continue
+ if not (subdir == 'bin' or is_git_file(ffn)):
+ continue
+
+ if filename_filter is not None:
+ if not filename_filter(ffn):
+ continue
+
+ if shebang_filter is not None:
+ try:
+ f = open(ffn, 'rb')
+ except OSError as e:
+ print("could not open %s: %s" % (ffn, e))
+ continue
+ line = f.read(40)
+ f.close()
+ if not shebang_filter(line):
+ continue
+
+ name = safename('_', fn)
+ while name in cache:
+ name += '_'
+ cache[name] = ffn
+
+ return cache.items()
+
+# For ELF we only look at /bin/* top level.
+def elf_file_name(fn):
+ fn = fn.partition('bin/')[2]
+ return fn and '/' not in fn and 'test' not in fn and 'ldb' in fn
+
+def elf_shebang(x):
+ return x[:4] == b'\x7fELF'
+
+elf_cache = {}
+def elf_iterator():
+ return script_iterator(BASEDIR, elf_cache,
+ shebang_filter=elf_shebang,
+ filename_filter=elf_file_name,
+ subdirs=['bin'])
+
+
+perl_shebang = re.compile(br'#!.+perl').match
+
+perl_script_cache = {}
+def perl_script_iterator():
+ return script_iterator(BASEDIR, perl_script_cache, perl_shebang)
+
+
+python_shebang = re.compile(br'#!.+python').match
+
+python_script_cache = {}
+def python_script_iterator():
+ return script_iterator(BASEDIR, python_script_cache, python_shebang)
+
+
+class PerlScriptUsageTests(TestCase):
+ """Perl scripts run without arguments should print a usage string,
+ not fail with a traceback.
+ """
+
+ @classmethod
+ def initialise(cls):
+ for name, filename in perl_script_iterator():
+ print(name, filename)
+
+
+class PythonScriptUsageTests(TestCase):
+ """Python scripts run without arguments should print a usage string,
+ not fail with a traceback.
+ """
+
+ @classmethod
+ def initialise(cls):
+ for name, filename in python_script_iterator():
+ # We add the actual tests after the class definition so we
+ # can give individual names to them, so we can have a
+ # knownfail list.
+ fn = filename.replace(BASEDIR, '').lstrip('/')
+
+ if fn in EXCLUDE_USAGE:
+ print("skipping %s (EXCLUDE_USAGE)" % filename)
+ continue
+
+ if os.path.dirname(fn) in EXCLUDE_DIRS:
+ print("skipping %s (EXCLUDE_DIRS)" % filename)
+ continue
+
+ def _f(self, filename=filename):
+ print(filename)
+ try:
+ p = subprocess.Popen(['python3', filename],
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ out, err = p.communicate(timeout=5)
+ except OSError as e:
+ self.fail("Error: %s" % e)
+ except subprocess.SubprocessError as e:
+ self.fail("Subprocess error: %s" % e)
+
+ err = err.decode('utf-8')
+ out = out.decode('utf-8')
+ self.assertNotIn('Traceback', err)
+
+ self.assertIn('usage', out.lower() + err.lower(),
+ 'stdout:\n%s\nstderr:\n%s' % (out, err))
+
+ attr = 'test_%s' % name
+ if hasattr(cls, attr):
+ raise RuntimeError(f'Usage test ‘{attr}’ already exists!')
+ setattr(cls, attr, _f)
+
+
+class HelpTestSuper(TestCase):
+ """Python scripts run with -h or --help should print a help string,
+ and exit with success.
+ """
+ check_return_code = True
+ check_consistency = True
+ check_contains_usage = True
+ check_multiline = True
+ check_merged_out_and_err = False
+
+ interpreter = None
+
+ options_start = None
+ options_end = None
+ def iterator(self):
+ raise NotImplementedError("Subclass this "
+ "and add an iterator function!")
+
+ @classmethod
+ def initialise(cls):
+ for name, filename in cls.iterator():
+ # We add the actual tests after the class definition so we
+ # can give individual names to them, so we can have a
+ # knownfail list.
+ fn = filename.replace(BASEDIR, '').lstrip('/')
+
+ if fn in EXCLUDE_HELP:
+ print("skipping %s (EXCLUDE_HELP)" % filename)
+ continue
+
+ if os.path.dirname(fn) in EXCLUDE_DIRS:
+ print("skipping %s (EXCLUDE_DIRS)" % filename)
+ continue
+
+ def _f(self, filename=filename):
+ print(filename)
+ for h in ('--help', '-h'):
+ cmd = [filename, h]
+ if self.interpreter:
+ cmd.insert(0, self.interpreter)
+ try:
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ out, err = p.communicate(timeout=5)
+ except OSError as e:
+ self.fail("Error: %s" % e)
+ except subprocess.SubprocessError as e:
+ self.fail("Subprocess error: %s" % e)
+
+ err = err.decode('utf-8')
+ out = out.decode('utf-8')
+ if self.check_merged_out_and_err:
+ out = "%s\n%s" % (out, err)
+
+ outl = out[:500].lower()
+ # NOTE:
+ # These assertions are heuristics, not policy.
+ # If your script fails this test when it shouldn't
+ # just add it to EXCLUDE_HELP above or change the
+ # heuristic.
+
+ # --help should produce:
+ # * multiple lines of help on stdout (not stderr),
+ # * including a "Usage:" string,
+ # * not contradict itself or repeat options,
+ # * and return success.
+ #print(out.encode('utf8'))
+ #print(err.encode('utf8'))
+ if self.check_consistency:
+ errors = check_help_consistency(out,
+ self.options_start,
+ self.options_end)
+ if errors is not None:
+ self.fail(errors)
+
+ if self.check_return_code:
+ self.assertEqual(p.returncode, 0,
+ "%s %s\nreturncode should not be %d\n"
+ "err:\n%s\nout:\n%s" %
+ (filename, h, p.returncode, err, out))
+ if self.check_contains_usage:
+ self.assertIn('usage', outl, 'lacks "Usage:"\n')
+ if self.check_multiline:
+ self.assertIn('\n', out, 'expected multi-line output')
+
+ attr = 'test_%s' % name
+ if hasattr(cls, attr):
+ raise RuntimeError(f'Usage test ‘{attr}’ already exists!')
+ setattr(cls, attr, _f)
+
+
+class PythonScriptHelpTests(HelpTestSuper):
+ """Python scripts run with -h or --help should print a help string,
+ and exit with success.
+ """
+ iterator = python_script_iterator
+ interpreter = 'python3'
+
+
+class ElfHelpTests(HelpTestSuper):
+ """ELF binaries run with -h or --help should print a help string,
+ and exit with success.
+ """
+ iterator = elf_iterator
+ check_return_code = False
+ check_merged_out_and_err = True
+
+
+PerlScriptUsageTests.initialise()
+PythonScriptUsageTests.initialise()
+PythonScriptHelpTests.initialise()
+ElfHelpTests.initialise()