import argparse
import glob
import os
import sys
from subprocess import PIPE, DEVNULL, STDOUT
class Runcmd:
def __init__(self, verbose, progress):
self._verbose = verbose
self._progress = progress
self._env = {}
def _write_msg(self, msg):
sys.stdout.write(f"{msg}\n")
sys.stdout.flush()
def setenv(self, key, value):
self._env[key] = value
def msg(self, msg):
if self._verbose:
self._write_msg(msg)
def title(self, title):
if self._verbose:
self._write_msg("")
self._write_msg("=" * len(title))
self._write_msg(title)
self._write_msg("=" * len(title))
elif self._progress:
self._write_msg(title)
def _runcmd_unchecked(self, argv, **kwargs):
from subprocess import run
self.msg(f"RUN: {argv} {kwargs}")
if not self._verbose:
kwargs["stdout"] = PIPE
kwargs["stderr"] = STDOUT
assert "key" not in kwargs
env = dict(os.environ)
env.update(self._env)
return run(argv, env=env, **kwargs)
def runcmd(self, argv, **kwargs):
p = self._runcmd_unchecked(argv, **kwargs)
if p.returncode != 0:
sys.stdout.write((p.stdout or b"").decode("UTF-8"))
sys.stderr.write((p.stderr or b"").decode("UTF-8"))
sys.stderr.write(f"Command {argv} failed\n")
sys.exit(p.returncode)
return p
def runcmd_maybe(self, argv, **kwargs):
if self.got_command(argv[0]):
self.runcmd(argv, **kwargs)
def got_command(self, name):
p = self._runcmd_unchecked(["which", name], stdout=DEVNULL)
return p.returncode == 0
def cargo(self, args, **kwargs):
self.runcmd(["cargo"] + args, **kwargs)
def cargo_maybe(self, args, **kwargs):
if self.got_cargo(args[0]):
self.runcmd(["cargo"] + args, **kwargs)
def got_cargo(self, subcommand):
p = self.runcmd(["cargo", "--list"], check=True, stdout=PIPE)
lines = [line.strip() for line in p.stdout.decode("UTF-8").splitlines()]
return subcommand in lines
def codegen(self, md, output, **kwargs):
self.cargo(
[
"run",
"--package=subplot",
"--bin=subplot",
"--",
f"--resources={os.path.abspath('share')}",
"codegen",
md,
f"--output={output}",
],
**kwargs,
)
def docgen(self, md, output, **kwargs):
self.cargo(
[
"run",
"--package=subplot",
"--bin=subplot",
"--",
f"--resources={os.path.abspath('share')}",
"docgen",
md,
f"--output={output}",
],
**kwargs,
)
def find_files(pattern, pred):
return [f for f in glob.glob(pattern, recursive=True) if pred(f)]
def check_python(r):
r.title("checking Python code")
py = find_files(
"**/*.py",
lambda f: os.path.basename(f) not in ("template.py", "test.py")
and "test-outputs" not in f,
)
r.runcmd_maybe(["flake8", "check"] + py)
r.runcmd_maybe(["black", "--check"] + py)
tests = find_files("**/*_tests.py", lambda f: True)
for test in tests:
dirname = os.path.dirname(test)
test = os.path.basename(test)
r.runcmd(["python3", test], cwd=dirname)
def check_shell(r):
r.title("checking shell code")
sh = find_files(
"**/*.sh",
lambda f: os.path.basename(f) != "test.sh" and "test-outputs" not in f,
)
r.runcmd_maybe(["shellcheck"] + sh)
def check_rust(r, strict=False):
r.title("checking Rust code")
r.runcmd(["cargo", "build", "--all-targets"])
if r.got_cargo("clippy"):
if strict:
r.runcmd(["cargo", "clippy", "--all-targets", "--", "-D", "warnings"])
else:
r.runcmd(["cargo", "clippy", "--all-targets"])
elif strict:
sys.exit("Strict Rust checks specified, but clippy was not found")
r.runcmd(["cargo", "test"])
r.runcmd(["cargo", "fmt", "--", "--check"])
def check_subplots(r):
output = os.path.abspath("test-outputs")
os.makedirs(output, exist_ok=True)
mds = find_files(
"**/*.md",
lambda f: f == f.lower() and "subplotlib" not in f and "test-outputs" not in f,
)
for md0 in mds:
r.title(f"checking subplot {md0}")
dirname = os.path.dirname(md0) or "."
md = os.path.basename(md0)
base, _ = os.path.splitext(md)
template = get_template(md0)
if template == "python":
test_py = os.path.join(output, f"test-{base}.py")
test_log = os.path.join(output, f"test-{base}.log")
if os.path.exists(test_log):
os.remove(test_log)
r.codegen(md, test_py, cwd=dirname)
r.runcmd(["python3", test_py, "--log", test_log], cwd=dirname)
elif template == "bash":
test_sh = os.path.join(output, f"test-{base}.sh")
r.codegen(md, test_sh, cwd=dirname)
r.runcmd(["bash", "-x", test_sh], cwd=dirname)
else:
sys.exit(f"unknown template {template} in {md0}")
base = os.path.basename(md)
base, _ = os.path.splitext(md)
base = os.path.join(output, base)
r.docgen(md, base + ".pdf", cwd=dirname)
r.docgen(md, base + ".html", cwd=dirname)
def check_subplotlib(r):
r.title("checking subplotlib code")
output = os.path.abspath("test-outputs/subplotlib")
os.makedirs(output, exist_ok=True)
r.runcmd(["cargo", "test", "--lib"], cwd="subplotlib")
r.runcmd(["cargo", "test", "--doc"], cwd="subplotlib")
mds = find_files("subplotlib/*.md", lambda f: True)
os.makedirs("subplotlib/tests", exist_ok=True)
for md0 in mds:
r.title(f"checking subplot {md0}")
dirname = os.path.dirname(md0)
md = os.path.basename(md0)
base, _ = os.path.splitext(md)
test_rs = os.path.join("tests", base + ".rs")
r.codegen(md, test_rs, cwd=dirname)
r.docgen(md, os.path.join(output, base + ".html"), cwd=dirname)
r.docgen(md, os.path.join(output, base + ".pdf"), cwd=dirname)
r.title("Formatting subplotlib")
r.cargo(["fmt", "-p", "subplotlib"], cwd=dirname)
r.title("Running subplotlib integration tests")
r.cargo(["test", "-p", "subplotlib", "--tests"])
def check_tooling(r):
commands = [
"bash",
"cargo",
"dot",
"pandoc",
"pandoc-citeproc",
"pdflatex",
"plantuml",
"rustc",
]
for command in commands:
if not r.got_command(command):
sys.exit(f"can't find {command}, which is needed for test suite")
if not r.got_command("daemonize") and not r.got_command("/usr/sbin/daemonize"):
sys.exit(
"can't find daemonize in PATH or in /usr/sbin, but it's needed for test suite"
)
def get_template(filename):
prefix = "template: "
with open(filename) as f:
data = f.read()
for line in data.splitlines():
if line.startswith(prefix):
line = line[len(prefix) :]
return line
sys.exit(f"{filename} does not specify a template")
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("-v", dest="verbose", action="store_true", help="be verbose")
p.add_argument(
"-p", dest="progress", action="store_true", help="print some progress output"
)
p.add_argument(
"--strict", action="store_true", help="don't allow compiler warnings"
)
all_whats = ["tooling", "python", "shell", "rust", "subplots", "subplotlib"]
p.add_argument(
"what", nargs="*", default=all_whats, help=f"what to test: {all_whats}"
)
return p.parse_args()
def main():
args = parse_args()
r = Runcmd(args.verbose, args.progress)
r.setenv("PYTHONDONTWRITEBYTECODE", "1")
for what in args.what:
if what == "python":
check_python(r)
elif what == "shell":
check_shell(r)
elif what == "rust":
check_rust(r, strict=args.strict)
elif what == "subplots":
check_subplots(r)
elif what == "subplotlib":
check_subplotlib(r)
elif what == "tooling":
check_tooling(r)
else:
sys.exit(f"Unknown test {what}")
sys.stdout.write("Everything seems to be in order.\n")
main()