1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
#!/usr/bin/env python3
"""Run a test repeatedly with mallocfail.
Usage: run_mallocfail [--ctest=<cost>] [<exe>...]
This runs the programs with mallocfail until there are no more additional
stack hashes for mallocfail to try out.
Passing "--ctest" additionally runs all the tests with a cost lower than the
given flag value. Cost is measured in seconds of runtime.
You need to build mallocfail (https://github.com/ralight/mallocfail) and install
it to /usr/local/lib/mallocfail. Change _MALLOCFAIL_SO below if you want it
elsewhere.
"""
import glob
import multiprocessing
import os
import shutil
import subprocess
import sys
import tempfile
import time
from typing import Dict
from typing import List
from typing import NoReturn
from typing import Optional
from typing import Tuple
_PRIMER = "auto_tests/auto_version_test"
_MALLOCFAIL_SO = "/usr/local/lib/mallocfail.so"
_HASHES = "mallocfail_hashes"
_HASHES_PREV = "mallocfail_hashes.prev"
_TIMEOUT = 3.0
_BUILD_DIR = os.getcwd()
_ENV = {
"LD_PRELOAD": _MALLOCFAIL_SO,
"UBSAN_OPTIONS": "color=always,print_stacktrace=1,exitcode=11",
}
def run_mallocfail(tmpdir: str, timeout: float, exe: str, iteration: int,
keep_going: bool) -> bool:
"""Run a program with mallocfail."""
print(f"\x1b[1;33mmallocfail '{exe}' run #{iteration}\x1b[0m")
hashes = os.path.join(tmpdir, _HASHES)
hashes_prev = os.path.join(tmpdir, _HASHES_PREV)
profraw = os.path.join(_BUILD_DIR, "mallocfail.out", exe, "%p.profraw")
if os.path.exists(hashes):
shutil.copy(hashes, hashes_prev)
try:
proc = subprocess.run(
[exe],
timeout=timeout,
env={
"LLVM_PROFILE_FILE": profraw,
**_ENV,
},
cwd=tmpdir,
)
except subprocess.TimeoutExpired:
print(f"\x1b[1;34mProgram {exe} timed out\x1b[0m")
return True
finally:
assert os.path.exists(hashes)
if os.path.exists(hashes_prev):
with open(hashes_prev, "r") as prev:
with open(hashes, "r") as cur:
if prev.read() == cur.read():
# Done: no new stack hashes.
return False
if proc.returncode in (0, 1):
# Process exited cleanly (success or failure).
pass
elif proc.returncode == -6:
# abort(), we allow it.
pass
elif proc.returncode == 7:
# ck_assert failed, also fine for us.
pass
elif proc.returncode == -14:
print(f"\x1b[0;34mProgram '{exe}' timed out\x1b[0m")
else:
print(
f"\x1b[1;32mProgram '{exe}' failed to handle OOM situation cleanly (code {proc.returncode})\x1b[0m"
)
if not keep_going:
raise Exception("Aborting test")
return True
def ctest_costs() -> Dict[str, float]:
with open("Testing/Temporary/CTestCostData.txt", "r") as fh:
costs = {}
for line in fh.readlines():
if line.startswith("---"):
break
prog, _, cost = line.rstrip().split(" ")
costs[prog] = float(cost)
return costs
def find_prog(name: str) -> Tuple[Optional[str], ...]:
def attempt(path: str) -> Optional[str]:
if os.path.exists(path):
return path
return None
return (attempt(f"auto_tests/auto_{name}_test"),
) # attempt(f"./unit_{name}_test"),
def parse_flags(args: List[str]) -> Tuple[Dict[str, str], List[str]]:
flags: Dict[str, str] = {}
exes: List[str] = []
for arg in args:
if arg.startswith("--"):
flag, value = arg.split("=", 1)
flags[flag] = value
else:
exes.append(arg)
return flags, exes
def loop_mallocfail(tmpdir: str,
timeout: float,
exe: str,
keep_going: bool = False) -> None:
i = 1
while run_mallocfail(tmpdir, timeout, exe, i, keep_going):
i += 1
def isolated_mallocfail(timeout: int, exe: str) -> None:
with tempfile.TemporaryDirectory(prefix="mallocfail") as tmpdir:
print(f"\x1b[1;33mRunning for {exe} in isolated path {tmpdir}\x1b[0m")
os.mkdir(os.path.join(tmpdir, "auto_tests"))
shutil.copy(exe, os.path.join(tmpdir, exe))
shutil.copy(_HASHES, os.path.join(tmpdir, _HASHES))
loop_mallocfail(tmpdir, timeout, exe)
profraw = os.path.join(tmpdir, "default.profraw")
if os.path.exists(profraw):
shutil.copy(profraw, exe + ".mallocfail.profraw")
def main(args: List[str]) -> None:
"""Run a program repeatedly under mallocfail."""
if len(args) == 1:
print(f"Usage: {args[0]} <exe>")
sys.exit(1)
flags, exes = parse_flags(args[1:])
timeout = _TIMEOUT
if "--ctest" in flags:
costs = ctest_costs()
max_cost = float(flags["--ctest"])
timeout = max(max_cost + 1, timeout)
exes.extend(prog for test in costs.keys() for prog in find_prog(test)
if costs[test] <= max_cost and prog)
if "--jobs" in flags:
jobs = int(flags["--jobs"])
else:
jobs = 1
# Start by running version_test, which allocates no memory of its own, just
# to prime the mallocfail hashes so it doesn't make global initialisers
# such as llvm_gcov_init fail.
if os.path.exists(_PRIMER):
print(f"\x1b[1;33mPriming hashes with {_PRIMER}\x1b[0m")
loop_mallocfail(os.getcwd(), timeout, _PRIMER, keep_going=True)
print(f"\x1b[1;33m--------------------------------\x1b[0m")
print(f"\x1b[1;33mStarting mallocfail for {len(exes)} programs:\x1b[0m")
print(f"\x1b[1;33m{exes}\x1b[0m")
print(f"\x1b[1;33m--------------------------------\x1b[0m")
time.sleep(1)
with multiprocessing.Pool(jobs) as p:
done = tuple(
p.starmap(isolated_mallocfail, ((timeout, exe) for exe in exes)))
print(f"\x1b[1;32mCompleted {len(done)} programs\x1b[0m")
if __name__ == "__main__":
main(sys.argv)
|