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