1*3117ece4Schristos#!/usr/bin/env python3 2*3117ece4Schristos# ################################################################ 3*3117ece4Schristos# Copyright (c) Meta Platforms, Inc. and affiliates. 4*3117ece4Schristos# All rights reserved. 5*3117ece4Schristos# 6*3117ece4Schristos# This source code is licensed under both the BSD-style license (found in the 7*3117ece4Schristos# LICENSE file in the root directory of this source tree) and the GPLv2 (found 8*3117ece4Schristos# in the COPYING file in the root directory of this source tree). 9*3117ece4Schristos# You may select, at your option, one of the above-listed licenses. 10*3117ece4Schristos# ########################################################################## 11*3117ece4Schristos 12*3117ece4Schristosimport argparse 13*3117ece4Schristosimport contextlib 14*3117ece4Schristosimport copy 15*3117ece4Schristosimport fnmatch 16*3117ece4Schristosimport os 17*3117ece4Schristosimport shutil 18*3117ece4Schristosimport subprocess 19*3117ece4Schristosimport sys 20*3117ece4Schristosimport tempfile 21*3117ece4Schristosimport typing 22*3117ece4Schristos 23*3117ece4Schristos 24*3117ece4SchristosZSTD_SYMLINKS = [ 25*3117ece4Schristos "zstd", 26*3117ece4Schristos "zstdmt", 27*3117ece4Schristos "unzstd", 28*3117ece4Schristos "zstdcat", 29*3117ece4Schristos "zcat", 30*3117ece4Schristos "gzip", 31*3117ece4Schristos "gunzip", 32*3117ece4Schristos "gzcat", 33*3117ece4Schristos "lzma", 34*3117ece4Schristos "unlzma", 35*3117ece4Schristos "xz", 36*3117ece4Schristos "unxz", 37*3117ece4Schristos "lz4", 38*3117ece4Schristos "unlz4", 39*3117ece4Schristos] 40*3117ece4Schristos 41*3117ece4Schristos 42*3117ece4SchristosEXCLUDED_DIRS = { 43*3117ece4Schristos "bin", 44*3117ece4Schristos "common", 45*3117ece4Schristos "scratch", 46*3117ece4Schristos} 47*3117ece4Schristos 48*3117ece4Schristos 49*3117ece4SchristosEXCLUDED_BASENAMES = { 50*3117ece4Schristos "setup", 51*3117ece4Schristos "setup_once", 52*3117ece4Schristos "teardown", 53*3117ece4Schristos "teardown_once", 54*3117ece4Schristos "README.md", 55*3117ece4Schristos "run.py", 56*3117ece4Schristos ".gitignore", 57*3117ece4Schristos} 58*3117ece4Schristos 59*3117ece4SchristosEXCLUDED_SUFFIXES = [ 60*3117ece4Schristos ".exact", 61*3117ece4Schristos ".glob", 62*3117ece4Schristos ".ignore", 63*3117ece4Schristos ".exit", 64*3117ece4Schristos] 65*3117ece4Schristos 66*3117ece4Schristos 67*3117ece4Schristosdef exclude_dir(dirname: str) -> bool: 68*3117ece4Schristos """ 69*3117ece4Schristos Should files under the directory :dirname: be excluded from the test runner? 70*3117ece4Schristos """ 71*3117ece4Schristos if dirname in EXCLUDED_DIRS: 72*3117ece4Schristos return True 73*3117ece4Schristos return False 74*3117ece4Schristos 75*3117ece4Schristos 76*3117ece4Schristosdef exclude_file(filename: str) -> bool: 77*3117ece4Schristos """Should the file :filename: be excluded from the test runner?""" 78*3117ece4Schristos if filename in EXCLUDED_BASENAMES: 79*3117ece4Schristos return True 80*3117ece4Schristos for suffix in EXCLUDED_SUFFIXES: 81*3117ece4Schristos if filename.endswith(suffix): 82*3117ece4Schristos return True 83*3117ece4Schristos return False 84*3117ece4Schristos 85*3117ece4Schristosdef read_file(filename: str) -> bytes: 86*3117ece4Schristos """Reads the file :filename: and returns the contents as bytes.""" 87*3117ece4Schristos with open(filename, "rb") as f: 88*3117ece4Schristos return f.read() 89*3117ece4Schristos 90*3117ece4Schristos 91*3117ece4Schristosdef diff(a: bytes, b: bytes) -> str: 92*3117ece4Schristos """Returns a diff between two different byte-strings :a: and :b:.""" 93*3117ece4Schristos assert a != b 94*3117ece4Schristos with tempfile.NamedTemporaryFile("wb") as fa: 95*3117ece4Schristos fa.write(a) 96*3117ece4Schristos fa.flush() 97*3117ece4Schristos with tempfile.NamedTemporaryFile("wb") as fb: 98*3117ece4Schristos fb.write(b) 99*3117ece4Schristos fb.flush() 100*3117ece4Schristos 101*3117ece4Schristos diff_bytes = subprocess.run(["diff", fa.name, fb.name], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout 102*3117ece4Schristos return diff_bytes.decode("utf8") 103*3117ece4Schristos 104*3117ece4Schristos 105*3117ece4Schristosdef pop_line(data: bytes) -> typing.Tuple[typing.Optional[bytes], bytes]: 106*3117ece4Schristos """ 107*3117ece4Schristos Pop the first line from :data: and returns the first line and the remainder 108*3117ece4Schristos of the data as a tuple. If :data: is empty, returns :(None, data):. Otherwise 109*3117ece4Schristos the first line always ends in a :\n:, even if it is the last line and :data: 110*3117ece4Schristos doesn't end in :\n:. 111*3117ece4Schristos """ 112*3117ece4Schristos NEWLINE = b"\n" 113*3117ece4Schristos 114*3117ece4Schristos if data == b'': 115*3117ece4Schristos return (None, data) 116*3117ece4Schristos 117*3117ece4Schristos parts = data.split(NEWLINE, maxsplit=1) 118*3117ece4Schristos line = parts[0] + NEWLINE 119*3117ece4Schristos if len(parts) == 1: 120*3117ece4Schristos return line, b'' 121*3117ece4Schristos 122*3117ece4Schristos return line, parts[1] 123*3117ece4Schristos 124*3117ece4Schristos 125*3117ece4Schristosdef glob_line_matches(actual: bytes, expect: bytes) -> bool: 126*3117ece4Schristos """ 127*3117ece4Schristos Does the `actual` line match the expected glob line `expect`? 128*3117ece4Schristos """ 129*3117ece4Schristos return fnmatch.fnmatchcase(actual.strip(), expect.strip()) 130*3117ece4Schristos 131*3117ece4Schristos 132*3117ece4Schristosdef glob_diff(actual: bytes, expect: bytes) -> bytes: 133*3117ece4Schristos """ 134*3117ece4Schristos Returns None if the :actual: content matches the expected glob :expect:, 135*3117ece4Schristos otherwise returns the diff bytes. 136*3117ece4Schristos """ 137*3117ece4Schristos diff = b'' 138*3117ece4Schristos actual_line, actual = pop_line(actual) 139*3117ece4Schristos expect_line, expect = pop_line(expect) 140*3117ece4Schristos while True: 141*3117ece4Schristos # Handle end of file conditions - allow extra newlines 142*3117ece4Schristos while expect_line is None and actual_line == b"\n": 143*3117ece4Schristos actual_line, actual = pop_line(actual) 144*3117ece4Schristos while actual_line is None and expect_line == b"\n": 145*3117ece4Schristos expect_line, expect = pop_line(expect) 146*3117ece4Schristos 147*3117ece4Schristos if expect_line is None and actual_line is None: 148*3117ece4Schristos if diff == b'': 149*3117ece4Schristos return None 150*3117ece4Schristos return diff 151*3117ece4Schristos elif expect_line is None: 152*3117ece4Schristos diff += b"---\n" 153*3117ece4Schristos while actual_line != None: 154*3117ece4Schristos diff += b"> " 155*3117ece4Schristos diff += actual_line 156*3117ece4Schristos actual_line, actual = pop_line(actual) 157*3117ece4Schristos return diff 158*3117ece4Schristos elif actual_line is None: 159*3117ece4Schristos diff += b"---\n" 160*3117ece4Schristos while expect_line != None: 161*3117ece4Schristos diff += b"< " 162*3117ece4Schristos diff += expect_line 163*3117ece4Schristos expect_line, expect = pop_line(expect) 164*3117ece4Schristos return diff 165*3117ece4Schristos 166*3117ece4Schristos assert expect_line is not None 167*3117ece4Schristos assert actual_line is not None 168*3117ece4Schristos 169*3117ece4Schristos if expect_line == b'...\n': 170*3117ece4Schristos next_expect_line, expect = pop_line(expect) 171*3117ece4Schristos if next_expect_line is None: 172*3117ece4Schristos if diff == b'': 173*3117ece4Schristos return None 174*3117ece4Schristos return diff 175*3117ece4Schristos while not glob_line_matches(actual_line, next_expect_line): 176*3117ece4Schristos actual_line, actual = pop_line(actual) 177*3117ece4Schristos if actual_line is None: 178*3117ece4Schristos diff += b"---\n" 179*3117ece4Schristos diff += b"< " 180*3117ece4Schristos diff += next_expect_line 181*3117ece4Schristos return diff 182*3117ece4Schristos expect_line = next_expect_line 183*3117ece4Schristos continue 184*3117ece4Schristos 185*3117ece4Schristos if not glob_line_matches(actual_line, expect_line): 186*3117ece4Schristos diff += b'---\n' 187*3117ece4Schristos diff += b'< ' + expect_line 188*3117ece4Schristos diff += b'> ' + actual_line 189*3117ece4Schristos 190*3117ece4Schristos actual_line, actual = pop_line(actual) 191*3117ece4Schristos expect_line, expect = pop_line(expect) 192*3117ece4Schristos 193*3117ece4Schristos 194*3117ece4Schristosclass Options: 195*3117ece4Schristos """Options configuring how to run a :TestCase:.""" 196*3117ece4Schristos def __init__( 197*3117ece4Schristos self, 198*3117ece4Schristos env: typing.Dict[str, str], 199*3117ece4Schristos timeout: typing.Optional[int], 200*3117ece4Schristos verbose: bool, 201*3117ece4Schristos preserve: bool, 202*3117ece4Schristos scratch_dir: str, 203*3117ece4Schristos test_dir: str, 204*3117ece4Schristos set_exact_output: bool, 205*3117ece4Schristos ) -> None: 206*3117ece4Schristos self.env = env 207*3117ece4Schristos self.timeout = timeout 208*3117ece4Schristos self.verbose = verbose 209*3117ece4Schristos self.preserve = preserve 210*3117ece4Schristos self.scratch_dir = scratch_dir 211*3117ece4Schristos self.test_dir = test_dir 212*3117ece4Schristos self.set_exact_output = set_exact_output 213*3117ece4Schristos 214*3117ece4Schristos 215*3117ece4Schristosclass TestCase: 216*3117ece4Schristos """ 217*3117ece4Schristos Logic and state related to running a single test case. 218*3117ece4Schristos 219*3117ece4Schristos 1. Initialize the test case. 220*3117ece4Schristos 2. Launch the test case with :TestCase.launch():. 221*3117ece4Schristos This will start the test execution in a subprocess, but 222*3117ece4Schristos not wait for completion. So you could launch multiple test 223*3117ece4Schristos cases in parallel. This will now print any test output. 224*3117ece4Schristos 3. Analyze the results with :TestCase.analyze():. This will 225*3117ece4Schristos join the test subprocess, check the results against the 226*3117ece4Schristos expectations, and print the results to stdout. 227*3117ece4Schristos 228*3117ece4Schristos :TestCase.run(): is also provided which combines the launch & analyze 229*3117ece4Schristos steps for single-threaded use-cases. 230*3117ece4Schristos 231*3117ece4Schristos All other methods, prefixed with _, are private helper functions. 232*3117ece4Schristos """ 233*3117ece4Schristos def __init__(self, test_filename: str, options: Options) -> None: 234*3117ece4Schristos """ 235*3117ece4Schristos Initialize the :TestCase: for the test located in :test_filename: 236*3117ece4Schristos with the given :options:. 237*3117ece4Schristos """ 238*3117ece4Schristos self._opts = options 239*3117ece4Schristos self._test_file = test_filename 240*3117ece4Schristos self._test_name = os.path.normpath( 241*3117ece4Schristos os.path.relpath(test_filename, start=self._opts.test_dir) 242*3117ece4Schristos ) 243*3117ece4Schristos self._success = {} 244*3117ece4Schristos self._message = {} 245*3117ece4Schristos self._test_stdin = None 246*3117ece4Schristos self._scratch_dir = os.path.abspath(os.path.join(self._opts.scratch_dir, self._test_name)) 247*3117ece4Schristos 248*3117ece4Schristos @property 249*3117ece4Schristos def name(self) -> str: 250*3117ece4Schristos """Returns the unique name for the test.""" 251*3117ece4Schristos return self._test_name 252*3117ece4Schristos 253*3117ece4Schristos def launch(self) -> None: 254*3117ece4Schristos """ 255*3117ece4Schristos Launch the test case as a subprocess, but do not block on completion. 256*3117ece4Schristos This allows users to run multiple tests in parallel. Results aren't yet 257*3117ece4Schristos printed out. 258*3117ece4Schristos """ 259*3117ece4Schristos self._launch_test() 260*3117ece4Schristos 261*3117ece4Schristos def analyze(self) -> bool: 262*3117ece4Schristos """ 263*3117ece4Schristos Must be called after :TestCase.launch():. Joins the test subprocess and 264*3117ece4Schristos checks the results against expectations. Finally prints the results to 265*3117ece4Schristos stdout and returns the success. 266*3117ece4Schristos """ 267*3117ece4Schristos self._join_test() 268*3117ece4Schristos self._check_exit() 269*3117ece4Schristos self._check_stderr() 270*3117ece4Schristos self._check_stdout() 271*3117ece4Schristos self._analyze_results() 272*3117ece4Schristos return self._succeeded 273*3117ece4Schristos 274*3117ece4Schristos def run(self) -> bool: 275*3117ece4Schristos """Shorthand for combining both :TestCase.launch(): and :TestCase.analyze():.""" 276*3117ece4Schristos self.launch() 277*3117ece4Schristos return self.analyze() 278*3117ece4Schristos 279*3117ece4Schristos def _log(self, *args, **kwargs) -> None: 280*3117ece4Schristos """Logs test output.""" 281*3117ece4Schristos print(file=sys.stdout, *args, **kwargs) 282*3117ece4Schristos 283*3117ece4Schristos def _vlog(self, *args, **kwargs) -> None: 284*3117ece4Schristos """Logs verbose test output.""" 285*3117ece4Schristos if self._opts.verbose: 286*3117ece4Schristos print(file=sys.stdout, *args, **kwargs) 287*3117ece4Schristos 288*3117ece4Schristos def _test_environment(self) -> typing.Dict[str, str]: 289*3117ece4Schristos """ 290*3117ece4Schristos Returns the environment to be used for the 291*3117ece4Schristos test subprocess. 292*3117ece4Schristos """ 293*3117ece4Schristos # We want to omit ZSTD cli flags so tests will be consistent across environments 294*3117ece4Schristos env = {k: v for k, v in os.environ.items() if not k.startswith("ZSTD")} 295*3117ece4Schristos for k, v in self._opts.env.items(): 296*3117ece4Schristos self._vlog(f"${k}='{v}'") 297*3117ece4Schristos env[k] = v 298*3117ece4Schristos return env 299*3117ece4Schristos 300*3117ece4Schristos def _launch_test(self) -> None: 301*3117ece4Schristos """Launch the test subprocess, but do not join it.""" 302*3117ece4Schristos args = [os.path.abspath(self._test_file)] 303*3117ece4Schristos stdin_name = f"{self._test_file}.stdin" 304*3117ece4Schristos if os.path.exists(stdin_name): 305*3117ece4Schristos self._test_stdin = open(stdin_name, "rb") 306*3117ece4Schristos stdin = self._test_stdin 307*3117ece4Schristos else: 308*3117ece4Schristos stdin = subprocess.DEVNULL 309*3117ece4Schristos cwd = self._scratch_dir 310*3117ece4Schristos env = self._test_environment() 311*3117ece4Schristos self._test_process = subprocess.Popen( 312*3117ece4Schristos args=args, 313*3117ece4Schristos stdin=stdin, 314*3117ece4Schristos cwd=cwd, 315*3117ece4Schristos env=env, 316*3117ece4Schristos stderr=subprocess.PIPE, 317*3117ece4Schristos stdout=subprocess.PIPE 318*3117ece4Schristos ) 319*3117ece4Schristos 320*3117ece4Schristos def _join_test(self) -> None: 321*3117ece4Schristos """Join the test process and save stderr, stdout, and the exit code.""" 322*3117ece4Schristos (stdout, stderr) = self._test_process.communicate(timeout=self._opts.timeout) 323*3117ece4Schristos self._output = {} 324*3117ece4Schristos self._output["stdout"] = stdout 325*3117ece4Schristos self._output["stderr"] = stderr 326*3117ece4Schristos self._exit_code = self._test_process.returncode 327*3117ece4Schristos self._test_process = None 328*3117ece4Schristos if self._test_stdin is not None: 329*3117ece4Schristos self._test_stdin.close() 330*3117ece4Schristos self._test_stdin = None 331*3117ece4Schristos 332*3117ece4Schristos def _check_output_exact(self, out_name: str, expected: bytes, exact_name: str) -> None: 333*3117ece4Schristos """ 334*3117ece4Schristos Check the output named :out_name: for an exact match against the :expected: content. 335*3117ece4Schristos Saves the success and message. 336*3117ece4Schristos """ 337*3117ece4Schristos check_name = f"check_{out_name}" 338*3117ece4Schristos actual = self._output[out_name] 339*3117ece4Schristos if actual == expected: 340*3117ece4Schristos self._success[check_name] = True 341*3117ece4Schristos self._message[check_name] = f"{out_name} matches!" 342*3117ece4Schristos else: 343*3117ece4Schristos self._success[check_name] = False 344*3117ece4Schristos self._message[check_name] = f"{out_name} does not match!\n> diff expected actual\n{diff(expected, actual)}" 345*3117ece4Schristos 346*3117ece4Schristos if self._opts.set_exact_output: 347*3117ece4Schristos with open(exact_name, "wb") as f: 348*3117ece4Schristos f.write(actual) 349*3117ece4Schristos 350*3117ece4Schristos def _check_output_glob(self, out_name: str, expected: bytes) -> None: 351*3117ece4Schristos """ 352*3117ece4Schristos Check the output named :out_name: for a glob match against the :expected: glob. 353*3117ece4Schristos Saves the success and message. 354*3117ece4Schristos """ 355*3117ece4Schristos check_name = f"check_{out_name}" 356*3117ece4Schristos actual = self._output[out_name] 357*3117ece4Schristos diff = glob_diff(actual, expected) 358*3117ece4Schristos if diff is None: 359*3117ece4Schristos self._success[check_name] = True 360*3117ece4Schristos self._message[check_name] = f"{out_name} matches!" 361*3117ece4Schristos else: 362*3117ece4Schristos utf8_diff = diff.decode('utf8') 363*3117ece4Schristos self._success[check_name] = False 364*3117ece4Schristos self._message[check_name] = f"{out_name} does not match!\n> diff expected actual\n{utf8_diff}" 365*3117ece4Schristos 366*3117ece4Schristos def _check_output(self, out_name: str) -> None: 367*3117ece4Schristos """ 368*3117ece4Schristos Checks the output named :out_name: for a match against the expectation. 369*3117ece4Schristos We check for a .exact, .glob, and a .ignore file. If none are found we 370*3117ece4Schristos expect that the output should be empty. 371*3117ece4Schristos 372*3117ece4Schristos If :Options.preserve: was set then we save the scratch directory and 373*3117ece4Schristos save the stderr, stdout, and exit code to the scratch directory for 374*3117ece4Schristos debugging. 375*3117ece4Schristos """ 376*3117ece4Schristos if self._opts.preserve: 377*3117ece4Schristos # Save the output to the scratch directory 378*3117ece4Schristos actual_name = os.path.join(self._scratch_dir, f"{out_name}") 379*3117ece4Schristos with open(actual_name, "wb") as f: 380*3117ece4Schristos f.write(self._output[out_name]) 381*3117ece4Schristos 382*3117ece4Schristos exact_name = f"{self._test_file}.{out_name}.exact" 383*3117ece4Schristos glob_name = f"{self._test_file}.{out_name}.glob" 384*3117ece4Schristos ignore_name = f"{self._test_file}.{out_name}.ignore" 385*3117ece4Schristos 386*3117ece4Schristos if os.path.exists(exact_name): 387*3117ece4Schristos return self._check_output_exact(out_name, read_file(exact_name), exact_name) 388*3117ece4Schristos elif os.path.exists(glob_name): 389*3117ece4Schristos return self._check_output_glob(out_name, read_file(glob_name)) 390*3117ece4Schristos else: 391*3117ece4Schristos check_name = f"check_{out_name}" 392*3117ece4Schristos self._success[check_name] = True 393*3117ece4Schristos self._message[check_name] = f"{out_name} ignored!" 394*3117ece4Schristos 395*3117ece4Schristos def _check_stderr(self) -> None: 396*3117ece4Schristos """Checks the stderr output against the expectation.""" 397*3117ece4Schristos self._check_output("stderr") 398*3117ece4Schristos 399*3117ece4Schristos def _check_stdout(self) -> None: 400*3117ece4Schristos """Checks the stdout output against the expectation.""" 401*3117ece4Schristos self._check_output("stdout") 402*3117ece4Schristos 403*3117ece4Schristos def _check_exit(self) -> None: 404*3117ece4Schristos """ 405*3117ece4Schristos Checks the exit code against expectations. If a .exit file 406*3117ece4Schristos exists, we expect that the exit code matches the contents. 407*3117ece4Schristos Otherwise we expect the exit code to be zero. 408*3117ece4Schristos 409*3117ece4Schristos If :Options.preserve: is set we save the exit code to the 410*3117ece4Schristos scratch directory under the filename "exit". 411*3117ece4Schristos """ 412*3117ece4Schristos if self._opts.preserve: 413*3117ece4Schristos exit_name = os.path.join(self._scratch_dir, "exit") 414*3117ece4Schristos with open(exit_name, "w") as f: 415*3117ece4Schristos f.write(str(self._exit_code) + "\n") 416*3117ece4Schristos exit_name = f"{self._test_file}.exit" 417*3117ece4Schristos if os.path.exists(exit_name): 418*3117ece4Schristos exit_code: int = int(read_file(exit_name)) 419*3117ece4Schristos else: 420*3117ece4Schristos exit_code: int = 0 421*3117ece4Schristos if exit_code == self._exit_code: 422*3117ece4Schristos self._success["check_exit"] = True 423*3117ece4Schristos self._message["check_exit"] = "Exit code matches!" 424*3117ece4Schristos else: 425*3117ece4Schristos self._success["check_exit"] = False 426*3117ece4Schristos self._message["check_exit"] = f"Exit code mismatch! Expected {exit_code} but got {self._exit_code}" 427*3117ece4Schristos 428*3117ece4Schristos def _analyze_results(self) -> None: 429*3117ece4Schristos """ 430*3117ece4Schristos After all tests have been checked, collect all the successes 431*3117ece4Schristos and messages, and print the results to stdout. 432*3117ece4Schristos """ 433*3117ece4Schristos STATUS = {True: "PASS", False: "FAIL"} 434*3117ece4Schristos checks = sorted(self._success.keys()) 435*3117ece4Schristos self._succeeded = all(self._success.values()) 436*3117ece4Schristos self._log(f"{STATUS[self._succeeded]}: {self._test_name}") 437*3117ece4Schristos 438*3117ece4Schristos if not self._succeeded or self._opts.verbose: 439*3117ece4Schristos for check in checks: 440*3117ece4Schristos if self._opts.verbose or not self._success[check]: 441*3117ece4Schristos self._log(f"{STATUS[self._success[check]]}: {self._test_name}.{check}") 442*3117ece4Schristos self._log(self._message[check]) 443*3117ece4Schristos 444*3117ece4Schristos self._log("----------------------------------------") 445*3117ece4Schristos 446*3117ece4Schristos 447*3117ece4Schristosclass TestSuite: 448*3117ece4Schristos """ 449*3117ece4Schristos Setup & teardown test suite & cases. 450*3117ece4Schristos This class is intended to be used as a context manager. 451*3117ece4Schristos 452*3117ece4Schristos TODO: Make setup/teardown failure emit messages, not throw exceptions. 453*3117ece4Schristos """ 454*3117ece4Schristos def __init__(self, test_directory: str, options: Options) -> None: 455*3117ece4Schristos self._opts = options 456*3117ece4Schristos self._test_dir = os.path.abspath(test_directory) 457*3117ece4Schristos rel_test_dir = os.path.relpath(test_directory, start=self._opts.test_dir) 458*3117ece4Schristos assert not rel_test_dir.startswith(os.path.sep) 459*3117ece4Schristos self._scratch_dir = os.path.normpath(os.path.join(self._opts.scratch_dir, rel_test_dir)) 460*3117ece4Schristos 461*3117ece4Schristos def __enter__(self) -> 'TestSuite': 462*3117ece4Schristos self._setup_once() 463*3117ece4Schristos return self 464*3117ece4Schristos 465*3117ece4Schristos def __exit__(self, _exc_type, _exc_value, _traceback) -> None: 466*3117ece4Schristos self._teardown_once() 467*3117ece4Schristos 468*3117ece4Schristos @contextlib.contextmanager 469*3117ece4Schristos def test_case(self, test_basename: str) -> TestCase: 470*3117ece4Schristos """ 471*3117ece4Schristos Context manager for a test case in the test suite. 472*3117ece4Schristos Pass the basename of the test relative to the :test_directory:. 473*3117ece4Schristos """ 474*3117ece4Schristos assert os.path.dirname(test_basename) == "" 475*3117ece4Schristos try: 476*3117ece4Schristos self._setup(test_basename) 477*3117ece4Schristos test_filename = os.path.join(self._test_dir, test_basename) 478*3117ece4Schristos yield TestCase(test_filename, self._opts) 479*3117ece4Schristos finally: 480*3117ece4Schristos self._teardown(test_basename) 481*3117ece4Schristos 482*3117ece4Schristos def _remove_scratch_dir(self, dir: str) -> None: 483*3117ece4Schristos """Helper to remove a scratch directory with sanity checks""" 484*3117ece4Schristos assert "scratch" in dir 485*3117ece4Schristos assert dir.startswith(self._scratch_dir) 486*3117ece4Schristos assert os.path.exists(dir) 487*3117ece4Schristos shutil.rmtree(dir) 488*3117ece4Schristos 489*3117ece4Schristos def _setup_once(self) -> None: 490*3117ece4Schristos if os.path.exists(self._scratch_dir): 491*3117ece4Schristos self._remove_scratch_dir(self._scratch_dir) 492*3117ece4Schristos os.makedirs(self._scratch_dir) 493*3117ece4Schristos setup_script = os.path.join(self._test_dir, "setup_once") 494*3117ece4Schristos if os.path.exists(setup_script): 495*3117ece4Schristos self._run_script(setup_script, cwd=self._scratch_dir) 496*3117ece4Schristos 497*3117ece4Schristos def _teardown_once(self) -> None: 498*3117ece4Schristos assert os.path.exists(self._scratch_dir) 499*3117ece4Schristos teardown_script = os.path.join(self._test_dir, "teardown_once") 500*3117ece4Schristos if os.path.exists(teardown_script): 501*3117ece4Schristos self._run_script(teardown_script, cwd=self._scratch_dir) 502*3117ece4Schristos if not self._opts.preserve: 503*3117ece4Schristos self._remove_scratch_dir(self._scratch_dir) 504*3117ece4Schristos 505*3117ece4Schristos def _setup(self, test_basename: str) -> None: 506*3117ece4Schristos test_scratch_dir = os.path.join(self._scratch_dir, test_basename) 507*3117ece4Schristos assert not os.path.exists(test_scratch_dir) 508*3117ece4Schristos os.makedirs(test_scratch_dir) 509*3117ece4Schristos setup_script = os.path.join(self._test_dir, "setup") 510*3117ece4Schristos if os.path.exists(setup_script): 511*3117ece4Schristos self._run_script(setup_script, cwd=test_scratch_dir) 512*3117ece4Schristos 513*3117ece4Schristos def _teardown(self, test_basename: str) -> None: 514*3117ece4Schristos test_scratch_dir = os.path.join(self._scratch_dir, test_basename) 515*3117ece4Schristos assert os.path.exists(test_scratch_dir) 516*3117ece4Schristos teardown_script = os.path.join(self._test_dir, "teardown") 517*3117ece4Schristos if os.path.exists(teardown_script): 518*3117ece4Schristos self._run_script(teardown_script, cwd=test_scratch_dir) 519*3117ece4Schristos if not self._opts.preserve: 520*3117ece4Schristos self._remove_scratch_dir(test_scratch_dir) 521*3117ece4Schristos 522*3117ece4Schristos def _run_script(self, script: str, cwd: str) -> None: 523*3117ece4Schristos env = copy.copy(os.environ) 524*3117ece4Schristos for k, v in self._opts.env.items(): 525*3117ece4Schristos env[k] = v 526*3117ece4Schristos try: 527*3117ece4Schristos subprocess.run( 528*3117ece4Schristos args=[script], 529*3117ece4Schristos stdin=subprocess.DEVNULL, 530*3117ece4Schristos stdout=subprocess.PIPE, 531*3117ece4Schristos stderr=subprocess.PIPE, 532*3117ece4Schristos cwd=cwd, 533*3117ece4Schristos env=env, 534*3117ece4Schristos check=True, 535*3117ece4Schristos ) 536*3117ece4Schristos except subprocess.CalledProcessError as e: 537*3117ece4Schristos print(f"{script} failed with exit code {e.returncode}!") 538*3117ece4Schristos print(f"stderr:\n{e.stderr}") 539*3117ece4Schristos print(f"stdout:\n{e.stdout}") 540*3117ece4Schristos raise 541*3117ece4Schristos 542*3117ece4SchristosTestSuites = typing.Dict[str, typing.List[str]] 543*3117ece4Schristos 544*3117ece4Schristosdef get_all_tests(options: Options) -> TestSuites: 545*3117ece4Schristos """ 546*3117ece4Schristos Find all the test in the test directory and return the test suites. 547*3117ece4Schristos """ 548*3117ece4Schristos test_suites = {} 549*3117ece4Schristos for root, dirs, files in os.walk(options.test_dir, topdown=True): 550*3117ece4Schristos dirs[:] = [d for d in dirs if not exclude_dir(d)] 551*3117ece4Schristos test_cases = [] 552*3117ece4Schristos for file in files: 553*3117ece4Schristos if not exclude_file(file): 554*3117ece4Schristos test_cases.append(file) 555*3117ece4Schristos assert root == os.path.normpath(root) 556*3117ece4Schristos test_suites[root] = test_cases 557*3117ece4Schristos return test_suites 558*3117ece4Schristos 559*3117ece4Schristos 560*3117ece4Schristosdef resolve_listed_tests( 561*3117ece4Schristos tests: typing.List[str], options: Options 562*3117ece4Schristos) -> TestSuites: 563*3117ece4Schristos """ 564*3117ece4Schristos Resolve the list of tests passed on the command line into their 565*3117ece4Schristos respective test suites. Tests can either be paths, or test names 566*3117ece4Schristos relative to the test directory. 567*3117ece4Schristos """ 568*3117ece4Schristos test_suites = {} 569*3117ece4Schristos for test in tests: 570*3117ece4Schristos if not os.path.exists(test): 571*3117ece4Schristos test = os.path.join(options.test_dir, test) 572*3117ece4Schristos if not os.path.exists(test): 573*3117ece4Schristos raise RuntimeError(f"Test {test} does not exist!") 574*3117ece4Schristos 575*3117ece4Schristos test = os.path.normpath(os.path.abspath(test)) 576*3117ece4Schristos assert test.startswith(options.test_dir) 577*3117ece4Schristos test_suite = os.path.dirname(test) 578*3117ece4Schristos test_case = os.path.basename(test) 579*3117ece4Schristos test_suites.setdefault(test_suite, []).append(test_case) 580*3117ece4Schristos 581*3117ece4Schristos return test_suites 582*3117ece4Schristos 583*3117ece4Schristosdef run_tests(test_suites: TestSuites, options: Options) -> bool: 584*3117ece4Schristos """ 585*3117ece4Schristos Runs all the test in the :test_suites: with the given :options:. 586*3117ece4Schristos Prints the results to stdout. 587*3117ece4Schristos """ 588*3117ece4Schristos tests = {} 589*3117ece4Schristos for test_dir, test_files in test_suites.items(): 590*3117ece4Schristos with TestSuite(test_dir, options) as test_suite: 591*3117ece4Schristos test_files = sorted(set(test_files)) 592*3117ece4Schristos for test_file in test_files: 593*3117ece4Schristos with test_suite.test_case(test_file) as test_case: 594*3117ece4Schristos tests[test_case.name] = test_case.run() 595*3117ece4Schristos 596*3117ece4Schristos successes = 0 597*3117ece4Schristos for test, status in tests.items(): 598*3117ece4Schristos if status: 599*3117ece4Schristos successes += 1 600*3117ece4Schristos else: 601*3117ece4Schristos print(f"FAIL: {test}") 602*3117ece4Schristos if successes == len(tests): 603*3117ece4Schristos print(f"PASSED all {len(tests)} tests!") 604*3117ece4Schristos return True 605*3117ece4Schristos else: 606*3117ece4Schristos print(f"FAILED {len(tests) - successes} / {len(tests)} tests!") 607*3117ece4Schristos return False 608*3117ece4Schristos 609*3117ece4Schristos 610*3117ece4Schristosdef setup_zstd_symlink_dir(zstd_symlink_dir: str, zstd: str) -> None: 611*3117ece4Schristos assert os.path.join("bin", "symlinks") in zstd_symlink_dir 612*3117ece4Schristos if not os.path.exists(zstd_symlink_dir): 613*3117ece4Schristos os.makedirs(zstd_symlink_dir) 614*3117ece4Schristos for symlink in ZSTD_SYMLINKS: 615*3117ece4Schristos path = os.path.join(zstd_symlink_dir, symlink) 616*3117ece4Schristos if os.path.exists(path): 617*3117ece4Schristos os.remove(path) 618*3117ece4Schristos os.symlink(zstd, path) 619*3117ece4Schristos 620*3117ece4Schristosif __name__ == "__main__": 621*3117ece4Schristos CLI_TEST_DIR = os.path.dirname(sys.argv[0]) 622*3117ece4Schristos REPO_DIR = os.path.join(CLI_TEST_DIR, "..", "..") 623*3117ece4Schristos PROGRAMS_DIR = os.path.join(REPO_DIR, "programs") 624*3117ece4Schristos TESTS_DIR = os.path.join(REPO_DIR, "tests") 625*3117ece4Schristos ZSTD_PATH = os.path.join(PROGRAMS_DIR, "zstd") 626*3117ece4Schristos ZSTDGREP_PATH = os.path.join(PROGRAMS_DIR, "zstdgrep") 627*3117ece4Schristos ZSTDLESS_PATH = os.path.join(PROGRAMS_DIR, "zstdless") 628*3117ece4Schristos DATAGEN_PATH = os.path.join(TESTS_DIR, "datagen") 629*3117ece4Schristos 630*3117ece4Schristos parser = argparse.ArgumentParser( 631*3117ece4Schristos ( 632*3117ece4Schristos "Runs the zstd CLI tests. Exits nonzero on failure. Default arguments are\n" 633*3117ece4Schristos "generally correct. Pass --preserve to preserve test output for debugging,\n" 634*3117ece4Schristos "and --verbose to get verbose test output.\n" 635*3117ece4Schristos ) 636*3117ece4Schristos ) 637*3117ece4Schristos parser.add_argument( 638*3117ece4Schristos "--preserve", 639*3117ece4Schristos action="store_true", 640*3117ece4Schristos help="Preserve the scratch directory TEST_DIR/scratch/ for debugging purposes." 641*3117ece4Schristos ) 642*3117ece4Schristos parser.add_argument("--verbose", action="store_true", help="Verbose test output.") 643*3117ece4Schristos parser.add_argument("--timeout", default=200, type=int, help="Test case timeout in seconds. Set to 0 to disable timeouts.") 644*3117ece4Schristos parser.add_argument( 645*3117ece4Schristos "--exec-prefix", 646*3117ece4Schristos default=None, 647*3117ece4Schristos help="Sets the EXEC_PREFIX environment variable. Prefix to invocations of the zstd CLI." 648*3117ece4Schristos ) 649*3117ece4Schristos parser.add_argument( 650*3117ece4Schristos "--zstd", 651*3117ece4Schristos default=ZSTD_PATH, 652*3117ece4Schristos help="Sets the ZSTD_BIN environment variable. Path of the zstd CLI." 653*3117ece4Schristos ) 654*3117ece4Schristos parser.add_argument( 655*3117ece4Schristos "--zstdgrep", 656*3117ece4Schristos default=ZSTDGREP_PATH, 657*3117ece4Schristos help="Sets the ZSTDGREP_BIN environment variable. Path of the zstdgrep CLI." 658*3117ece4Schristos ) 659*3117ece4Schristos parser.add_argument( 660*3117ece4Schristos "--zstdless", 661*3117ece4Schristos default=ZSTDLESS_PATH, 662*3117ece4Schristos help="Sets the ZSTDLESS_BIN environment variable. Path of the zstdless CLI." 663*3117ece4Schristos ) 664*3117ece4Schristos parser.add_argument( 665*3117ece4Schristos "--datagen", 666*3117ece4Schristos default=DATAGEN_PATH, 667*3117ece4Schristos help="Sets the DATAGEN_BIN environment variable. Path to the datagen CLI." 668*3117ece4Schristos ) 669*3117ece4Schristos parser.add_argument( 670*3117ece4Schristos "--test-dir", 671*3117ece4Schristos default=CLI_TEST_DIR, 672*3117ece4Schristos help=( 673*3117ece4Schristos "Runs the tests under this directory. " 674*3117ece4Schristos "Adds TEST_DIR/bin/ to path. " 675*3117ece4Schristos "Scratch directory located in TEST_DIR/scratch/." 676*3117ece4Schristos ) 677*3117ece4Schristos ) 678*3117ece4Schristos parser.add_argument( 679*3117ece4Schristos "--set-exact-output", 680*3117ece4Schristos action="store_true", 681*3117ece4Schristos help="Set stderr.exact and stdout.exact for all failing tests, unless .ignore or .glob already exists" 682*3117ece4Schristos ) 683*3117ece4Schristos parser.add_argument( 684*3117ece4Schristos "tests", 685*3117ece4Schristos nargs="*", 686*3117ece4Schristos help="Run only these test cases. Can either be paths or test names relative to TEST_DIR/" 687*3117ece4Schristos ) 688*3117ece4Schristos args = parser.parse_args() 689*3117ece4Schristos 690*3117ece4Schristos if args.timeout <= 0: 691*3117ece4Schristos args.timeout = None 692*3117ece4Schristos 693*3117ece4Schristos args.test_dir = os.path.normpath(os.path.abspath(args.test_dir)) 694*3117ece4Schristos bin_dir = os.path.abspath(os.path.join(args.test_dir, "bin")) 695*3117ece4Schristos zstd_symlink_dir = os.path.join(bin_dir, "symlinks") 696*3117ece4Schristos scratch_dir = os.path.join(args.test_dir, "scratch") 697*3117ece4Schristos 698*3117ece4Schristos setup_zstd_symlink_dir(zstd_symlink_dir, os.path.abspath(args.zstd)) 699*3117ece4Schristos 700*3117ece4Schristos env = {} 701*3117ece4Schristos if args.exec_prefix is not None: 702*3117ece4Schristos env["EXEC_PREFIX"] = args.exec_prefix 703*3117ece4Schristos env["ZSTD_SYMLINK_DIR"] = zstd_symlink_dir 704*3117ece4Schristos env["ZSTD_REPO_DIR"] = os.path.abspath(REPO_DIR) 705*3117ece4Schristos env["DATAGEN_BIN"] = os.path.abspath(args.datagen) 706*3117ece4Schristos env["ZSTDGREP_BIN"] = os.path.abspath(args.zstdgrep) 707*3117ece4Schristos env["ZSTDLESS_BIN"] = os.path.abspath(args.zstdless) 708*3117ece4Schristos env["COMMON"] = os.path.abspath(os.path.join(args.test_dir, "common")) 709*3117ece4Schristos env["PATH"] = bin_dir + ":" + os.getenv("PATH", "") 710*3117ece4Schristos env["LC_ALL"] = "C" 711*3117ece4Schristos 712*3117ece4Schristos opts = Options( 713*3117ece4Schristos env=env, 714*3117ece4Schristos timeout=args.timeout, 715*3117ece4Schristos verbose=args.verbose, 716*3117ece4Schristos preserve=args.preserve, 717*3117ece4Schristos test_dir=args.test_dir, 718*3117ece4Schristos scratch_dir=scratch_dir, 719*3117ece4Schristos set_exact_output=args.set_exact_output, 720*3117ece4Schristos ) 721*3117ece4Schristos 722*3117ece4Schristos if len(args.tests) == 0: 723*3117ece4Schristos tests = get_all_tests(opts) 724*3117ece4Schristos else: 725*3117ece4Schristos tests = resolve_listed_tests(args.tests, opts) 726*3117ece4Schristos 727*3117ece4Schristos success = run_tests(tests, opts) 728*3117ece4Schristos if success: 729*3117ece4Schristos sys.exit(0) 730*3117ece4Schristos else: 731*3117ece4Schristos sys.exit(1) 732