def checked_run(argv): """ Run a process with the given arguments. Log its output and raise an error if it fails. """ p = Run(argv) if p.status != 0: logging.error('Command failed: %s', ' '.join(quote_arg(arg) for arg in argv)) logging.error('Output:\n' + p.out) raise RuntimeError
def setup_result_dir(self): """Create the output directory in which the results are stored.""" if os.path.isdir(self.old_output_dir): rm(self.old_output_dir, True) if os.path.isdir(self.output_dir): mv(self.output_dir, self.old_output_dir) mkdir(self.output_dir) if self.main.args.dump_environ: with open(os.path.join(self.output_dir, 'environ.sh'), 'w') as f: for var_name in sorted(os.environ): f.write('export %s=%s\n' % (var_name, quote_arg(os.environ[var_name])))
def setup_result_dir(self) -> None: """Create the output directory in which the results are stored.""" assert self.main.args if os.path.isdir(self.old_output_dir): rm(self.old_output_dir, True) if os.path.isdir(self.output_dir): mv(self.output_dir, self.old_output_dir) mkdir(self.output_dir) if self.main.args.dump_environ: with open(os.path.join(self.output_dir, "environ.sh"), "w") as f: for var_name in sorted(os.environ): f.write("export {}={}\n".format( var_name, quote_arg(os.environ[var_name])))
def run_alr(*args, **kwargs): """ Run "alr" with the given arguments. :param bool complain_on_error: If true and the subprocess exits with a non-zero status code, print information on the standard output (for debugging) and raise a CalledProcessError (to abort the test). :param bool quiet: If true (which is the default), append "-q" to the command line. :param bool debug: If true (which is the default), append "-d" to the command line. This ensures uncaught exceptions are logged instead of presenting a sanitized error intended for final users. :rtype: ProcessResult """ complain_on_error = kwargs.pop('complain_on_error', True) debug = kwargs.pop('debug', True) force = kwargs.pop('force', False) quiet = kwargs.pop('quiet', True) if kwargs: first_unknown_kwarg = sorted(kwargs)[0] raise ValueError('Invalid argument: {}'.format(first_unknown_kwarg)) argv = ['alr'] argv.insert(1, '-n') # always non-interactive if debug: argv.insert(1, '-d') if force: argv.insert(1, '-f') if quiet: argv.insert(1, '-q') argv.extend(args) p = Run(argv) if p.status != 0 and complain_on_error: print('The following command:') print(' {}'.format(' '.join(quote_arg(arg) for arg in argv))) print('Exitted with status code {}'.format(p.status)) print('Output:') print(p.out) raise CalledProcessError('alr returned non-zero status code') # Convert CRLF line endings (Windows-style) to LF (Unix-style). This # canonicalization is necessary to make output comparison work on all # platforms. return ProcessResult(p.status, p.out.replace('\r\n', '\n'))
def setup_result_dirs(self) -> None: """Create the output directory in which the results are stored.""" assert self.main.args args = self.main.args # Both the actual new/old directories to use depend on both # --output-dir and --old-output-dir options. d = os.path.abspath(args.output_dir) if args.old_output_dir: self.output_dir = d old_output_dir = os.path.abspath(args.old_output_dir) else: self.output_dir = os.path.join(d, "new") old_output_dir = os.path.join(d, "old") # Rotate results directories if requested. In both cases, make sure the # new results dir is clean. if args.rotate_output_dirs: if os.path.isdir(old_output_dir): rm(old_output_dir, recursive=True) if os.path.isdir(self.output_dir): mv(self.output_dir, old_output_dir) elif os.path.isdir(self.output_dir): rm(self.output_dir, recursive=True) mkdir(self.output_dir) # Remember about the old output directory only if it exists and does # contain results. If not, this info will be unused at best, or lead to # incorrect behavior. self.old_output_dir = None if (os.path.exists(old_output_dir) and os.path.exists( os.path.join(old_output_dir, ReportIndex.INDEX_FILENAME))): self.old_output_dir = old_output_dir if args.dump_environ: with open(os.path.join(self.output_dir, "environ.sh"), "w") as f: for var_name in sorted(os.environ): f.write("export {}={}\n".format( var_name, quote_arg(os.environ[var_name])))
def write_comment_file(self, comment_file: IO[str]) -> None: # Sensible default: just write the command line used to run the # testsuite. Testsuites can override or reuse this. quoted_cmdline = " ".join(quote_arg(arg) for arg in sys.argv) comment_file.write(f"Testsuite options:" f"\n {quoted_cmdline}" f"\n")
def shell(self, args: List[str], cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None, catch_error: bool = True, analyze_output: bool = True, timeout: Optional[int] = None, encoding: Optional[str] = None, truncate_logs_threshold: Optional[int] = None) -> ProcessResult: """Run a subprocess. :param args: Arguments for the subprocess to run. :param cwd: Current working directory for the subprocess. By default (i.e. if None), use the test working directory. :param env: Environment to pass to the subprocess. :param catch_error: If True, consider that an error status code leads to a test failure. In that case, abort the testcase. :param analyze_output: If True, add the subprocess output to the ``self.output`` log. :param timeout: Timeout (in seconds) for the subprocess. Use ``self.default_timeout`` if left to None. :param encoding: Encoding to use when decoding the subprocess' output stream. If None, use the default enocding for this test (``self.default_encoding``, from the ``encoding`` entry in test.yaml). If "binary", leave the output undecoded as a bytes string. :param truncate_logs_threshold: Threshold to truncate the subprocess output in ``self.result.log``. See ``e3.testsuite.result.truncated``'s ``line_count`` argument. If left to None, use the testsuite's ``--truncate-logs`` option. """ # By default, run the subprocess in the test working directory if cwd is None: cwd = self.test_env["working_dir"] if timeout is None: timeout = self.default_process_timeout if truncate_logs_threshold is None: truncate_logs_threshold = self.testsuite_options.truncate_logs # Run the subprocess and log it def format_header(label: str, value: Any) -> str: return "{}{}{}: {}{}\n".format( self.Style.RESET_ALL + self.Style.BRIGHT, label, self.Style.RESET_ALL, self.Style.DIM, value, ) self.result.log += format_header( "Running", "{} (cwd={}{}{})".format(" ".join(quote_arg(a) for a in args), self.Style.RESET_ALL, cwd, self.Style.DIM)) process_info = {"cmd": args, "cwd": cwd} self.result.processes.append(process_info) # Python2's subprocess module does not handle timeout, so re-implement # e3.os.process's rlimit-based implementation of timeouts. if timeout is not None: args = [get_rlimit(), str(timeout)] + args # We cannot use e3.os.process.Run as this API forces the use of text # streams, whereas testsuite sometimes need to deal with binary data # (or unknown encodings, which is equivalent). subp = subprocess.Popen(args, cwd=cwd, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout: Union[str, bytes] stdout, _ = subp.communicate() assert isinstance(stdout, bytes) encoding = encoding or self.default_encoding if encoding != "binary": try: stdout = stdout.decode(encoding) except UnicodeDecodeError as exc: raise TestAbortWithError( "cannot decode process output ({}: {})".format( type(exc).__name__, exc)) p = ProcessResult(subp.returncode, stdout) self.result.log += format_header("Status code", p.status) process_info["status"] = p.status process_info["output"] = Log(stdout) self.result.log += format_header( "Output", "\n" + truncated(str(process_info["output"]), truncate_logs_threshold)) # If requested, use its output for analysis if analyze_output: self.output += stdout if catch_error and p.status != 0: raise TestAbortWithFailure("non-zero status code") return p
def shell( self, args: List[str], cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None, catch_error: bool = True, analyze_output: bool = True, timeout: Optional[int] = None, encoding: Optional[str] = None, truncate_logs_threshold: Optional[int] = None, ignore_environ: bool = True, ) -> ProcessResult: """Run a subprocess. :param args: Arguments for the subprocess to run. :param cwd: Current working directory for the subprocess. By default (i.e. if None), use the test working directory. :param env: Environment to pass to the subprocess. :param catch_error: If True, consider that an error status code leads to a test failure. In that case, abort the testcase. :param analyze_output: If True, add the subprocess output to the ``self.output`` log. :param timeout: Timeout (in seconds) for the subprocess. Use ``self.default_timeout`` if left to None. :param encoding: Encoding to use when decoding the subprocess' output stream. If None, use the default enocding for this test (``self.default_encoding``, from the ``encoding`` entry in test.yaml). If "binary", leave the output undecoded as a bytes string. :param truncate_logs_threshold: Threshold to truncate the subprocess output in ``self.result.log``. See ``e3.testsuite.result.truncated``'s ``line_count`` argument. If left to None, use the testsuite's ``--truncate-logs`` option. :param ignore_environ: Applies only when ``env`` is not None. When True (the default), pass exactly environment variables in ``env``. When False, pass a copy of ``os.environ`` that is augmented with variables in ``env``. """ # By default, run the subprocess in the test working directory if cwd is None: cwd = self.test_env["working_dir"] if timeout is None: timeout = self.default_process_timeout if truncate_logs_threshold is None: truncate_logs_threshold = self.testsuite_options.truncate_logs # Run the subprocess and log it def format_header(label: str, value: Any) -> str: return "{}{}{}: {}{}\n".format( self.Style.RESET_ALL + self.Style.BRIGHT, label, self.Style.RESET_ALL, self.Style.DIM, value, ) self.result.log += format_header( "Running", "{} (cwd={}{}{})".format( " ".join(quote_arg(a) for a in args), self.Style.RESET_ALL, cwd, self.Style.DIM, ), ) process_info = {"cmd": args, "cwd": cwd} self.result.processes.append(process_info) subp = Run( cmds=args, cwd=cwd, output=PIPE, error=STDOUT, input=DEVNULL, timeout=timeout, env=env, ignore_environ=ignore_environ, ) # Testsuites sometimes need to deal with binary data (or unknown # encodings, which is equivalent), so always use subp.raw_out. stdout: Union[str, bytes] stdout = subp.raw_out assert isinstance(stdout, bytes) encoding = encoding or self.default_encoding if encoding != "binary": try: stdout = stdout.decode(encoding) except UnicodeDecodeError as exc: raise TestAbortWithError( "cannot decode process output ({}: {})".format( type(exc).__name__, exc)) # We run subprocesses in foreground mode, so by the time Run's # constructor has returned, the subprocess is supposed to have # completed, and thus we are supposed to have an exit status code. assert subp.status is not None p = ProcessResult(subp.status, stdout) self.result.log += format_header("Status code", p.status) process_info["status"] = p.status process_info["output"] = Log(stdout) self.result.log += format_header( "Output", "\n" + truncated(str(process_info["output"]), truncate_logs_threshold), ) # If requested, use its output for analysis if analyze_output: self.output += stdout if catch_error and p.status != 0: raise TestAbortWithFailure("non-zero status code") return p