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