Created
February 21, 2024 02:06
-
-
Save walkerh/1783a8d8ae4a98efe21ebffc3203b1ff to your computer and use it in GitHub Desktop.
pytest bash functions demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func() { | |
echo hello | |
echo MARKER | |
echo -- $@ | |
var1=$1 | |
var2=$2 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[pytest] | |
addopts = --tb=no | |
testpaths = test* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
addict==2.4.0 | |
iniconfig==2.0.0 | |
packaging==23.2 | |
pluggy==1.4.0 | |
pytest==8.0.1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from base64 import b64decode | |
from dataclasses import dataclass | |
from pprint import pprint | |
from subprocess import PIPE, run | |
from addict import Dict | |
UTF8_SLASH_MODE = dict(encoding="utf-8", errors="backslashreplace") | |
PIPE_OUTPUTS = dict(stdout=PIPE, stderr=PIPE) | |
RUN_KWARGS = UTF8_SLASH_MODE | PIPE_OUTPUTS | |
@dataclass(frozen=True) | |
class BashFunctionCallResult: | |
returncode: int | |
stderr: str | |
function_output: str | |
namespace: Dict[str, str] | |
def call_bash_function(source_file, function, exports, *args) -> BashFunctionCallResult: | |
""" | |
Calls a bash function during construction and stores the results as members: | |
returncode, stderr, function_output, and namespace. The standard output from | |
the function goes into function_output. All variables named in exports | |
(a string or list) are exported from bash into namespace. | |
""" | |
command_list = build_command_list(source_file, function, exports, args) | |
result = run(["bash"], input=command_list, **RUN_KWARGS) | |
return convert_completed_process(result) | |
def build_command_list(source_file, function, exports, args): | |
commands = ( | |
start_bash_session(source_file) | |
+ [ | |
call_bash_function_command(function, args), | |
] | |
+ build_export_commands(exports) | |
) | |
# ^^^ For each variable we want to export from bash, serialize the value | |
# using base64 to prevent whitespace issues. | |
command_list = "\n".join(commands) | |
return command_list | |
def start_bash_session(source_file): | |
return [ | |
"set -Eeuo pipefail", | |
f"source {source_file}", | |
] | |
def call_bash_function_command(function, args): | |
argstr = " ".join([f"'{arg}'" for arg in args]) | |
return f"{function} {argstr}" | |
def build_export_commands(exports): | |
if isinstance(exports, str): | |
exports = exports.split() | |
return ["echo MARKER"] + [ | |
f'echo -n {name}=; echo -n "${name}" | base64' for name in exports | |
] | |
def convert_completed_process(result): | |
lines = result.stdout.splitlines() | |
# Use the LAST occurence of a line that is just MARKER to separate | |
# function output from exported variables: | |
marker_index = [i for i, s in enumerate(lines) if s == "MARKER"][-1] | |
function_output = "\n".join(lines[:marker_index]) | |
var_data = lines[marker_index + 1 :] | |
namespace = { | |
k: b64decode(v).decode() | |
for k, v in (record.split("=", 1) for record in var_data) | |
} | |
returncode = result.returncode | |
stderr = result.stderr | |
return BashFunctionCallResult(returncode, stderr, function_output, namespace) | |
def test_a(): | |
result = call_bash_function("lib.sh", "func", ["var1", "var2"], "foo\nbar", "spam") | |
assert result.stderr == "" | |
assert result.function_output == "hello\nMARKER\n-- foo bar spam" | |
assert result.namespace == {"var1": "foo\nbar", "var2": "spam"} | |
assert result.returncode == 0 | |
def test_b(): | |
result = call_bash_function("lib.sh", "func", "var1 var2", "foo\nbar", "spam") | |
assert result.stderr == "" | |
assert result.function_output == "hello\nMARKER\n-- foo bar spam" | |
assert result.namespace == {"var1": "foo\nbar", "var2": "spam"} | |
assert result.returncode == 0 | |
def test_c(): | |
result = call_bash_function("lib.sh", "func", "var2", "foo\nbar", "spam") | |
assert result.stderr == "" | |
assert result.function_output == "hello\nMARKER\n-- foo bar spam" | |
assert result.namespace == {"var2": "spam"} | |
assert result.returncode == 0 | |
def test_d(): | |
result = call_bash_function( | |
"lib.sh", "func", "var1 var2 var2", "foo\nbar", "sp''am", "eggs" | |
) | |
assert result.stderr == "" | |
assert result.function_output == "hello\nMARKER\n-- foo bar spam eggs" | |
assert result.namespace == {"var1": "foo\nbar", "var2": "spam"} | |
assert result.returncode == 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment