with_env now overrides the environment, similar to with_stdin an additional function update_env was created to mirror the prior functionality of with_env, updating the env This was changed as previously it was impossible to delete variables from the env replaced the code of .ok() with a call to .expect, to remove the code duplication Change-Id: I83933893c7f2ccfdc7bd4933b7592b475c435e76
142 lines
4.2 KiB
Python
142 lines
4.2 KiB
Python
import dataclasses
|
|
import json
|
|
import logging
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from functional2.testlib.terminal_code_eater import eat_terminal_codes
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class CommandResult:
|
|
cmd: list[str]
|
|
"""Command arguments which were run"""
|
|
rc: int
|
|
"""Return code"""
|
|
stderr: bytes
|
|
"""Outputted stderr"""
|
|
stdout: bytes
|
|
"""Outputted stdout"""
|
|
|
|
def ok(self) -> "CommandResult":
|
|
"""
|
|
assumes a return code of 0
|
|
:raises CalledProcessError: if the return code wasn't 0 and logs the processes stdout and stderr
|
|
"""
|
|
return self.expect(0)
|
|
|
|
def expect(self, rc: int) -> "CommandResult":
|
|
"""
|
|
assumes a return code of `rc`
|
|
:param rc: The expected return code
|
|
:raises CalledProcessError: if the return code wasn't `rc` and logs the processes stdout and stderr
|
|
"""
|
|
if self.rc != rc:
|
|
logger.error("stdout: %s", self.stdout_s)
|
|
logger.error("stderr: %s", self.stderr_s)
|
|
raise subprocess.CalledProcessError(
|
|
returncode=self.rc, cmd=self.cmd, stderr=self.stderr, output=self.stdout
|
|
)
|
|
return self
|
|
|
|
@property
|
|
def stdout_s(self) -> str:
|
|
"""Command stdout as str"""
|
|
return self.stdout.decode("utf-8", errors="replace")
|
|
|
|
@property
|
|
def stderr_s(self) -> str:
|
|
"""Command stderr as str"""
|
|
return self.stderr.decode("utf-8", errors="replace")
|
|
|
|
@property
|
|
def stdout_plain(self) -> str:
|
|
"""Command stderr as str with terminal escape sequences eaten and whitespace stripped"""
|
|
return eat_terminal_codes(self.stdout).decode("utf-8", errors="replace").strip()
|
|
|
|
@property
|
|
def stderr_plain(self) -> str:
|
|
"""Command stderr as str with terminal escape sequences eaten and whitespace stripped"""
|
|
return eat_terminal_codes(self.stderr).decode("utf-8", errors="replace").strip()
|
|
|
|
def json(self) -> Any:
|
|
"""
|
|
Assumes an ok() result and returns the Commands stdout parsed as json
|
|
:return: A parsed json object
|
|
"""
|
|
self.ok()
|
|
return json.loads(self.stdout)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Command:
|
|
"""
|
|
Provides a way of configuring a shell command and then running it
|
|
calls Popen internally
|
|
"""
|
|
|
|
argv: list[str]
|
|
"""
|
|
Arguments of the Process; argv[0] is the name of the binary
|
|
"""
|
|
env: dict[str, str] = dataclasses.field(default_factory=dict)
|
|
"""
|
|
environment variables; Note that `$PATH` is not added by default.
|
|
Use `.with_env(**os.environ.copy())` to add `$PATH`
|
|
"""
|
|
stdin: bytes | None = None
|
|
"""
|
|
Things to pipe into stdin of the process
|
|
"""
|
|
cwd: Path | None = None
|
|
"""
|
|
current-working-directory fo the process
|
|
"""
|
|
|
|
def with_env(self, **kwargs) -> "Command":
|
|
"""
|
|
sets the env to the given environment variables
|
|
:param kwargs: keyword arguments containing the environment
|
|
:return: self, command is chainable
|
|
"""
|
|
self.env = kwargs
|
|
return self
|
|
|
|
def update_env(self, **kwargs) -> "Command":
|
|
"""
|
|
updates the current environment with the given dict of variables
|
|
:param kwargs: new or updated environment variables
|
|
:return: self, command is chainable
|
|
"""
|
|
self.env.update(kwargs)
|
|
return self
|
|
|
|
def with_stdin(self, stdin: bytes) -> "Command":
|
|
"""
|
|
sets the input provided to stdin of the commands
|
|
:param stdin: data to pipe into stdin
|
|
:return: self, command is chainable
|
|
"""
|
|
self.stdin = stdin
|
|
return self
|
|
|
|
def run(self) -> CommandResult:
|
|
"""
|
|
Runs the configured command
|
|
:return: Information about the Result of the execution
|
|
"""
|
|
proc = subprocess.Popen(
|
|
self.argv,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
stdin=subprocess.PIPE if self.stdin else subprocess.DEVNULL,
|
|
cwd=self.cwd,
|
|
env=self.env,
|
|
)
|
|
(stdout, stderr) = proc.communicate(input=self.stdin)
|
|
rc = proc.returncode
|
|
return CommandResult(cmd=self.argv, rc=rc, stdout=stdout, stderr=stderr)
|