tests: add test suite implemented as a Python program

Signed-off-by: Lars Wirzenius <liw@liw.fi>
This commit is contained in:
Lars Wirzenius
2024-01-10 17:38:05 +02:00
parent ccf794d2a0
commit ba301ead4e
3 changed files with 343 additions and 6 deletions
+3 -5
View File
@@ -448,14 +448,12 @@ impl RunInfoBuilder {
Ok(RunInfo {
repo: self.repo.map(|x| x.to_string()).unwrap_or("".into()),
commit: self.commit.map(|x| x.to_string()).unwrap_or("".into()),
id: self.id.ok_or(NativeError::MissingInfo("id".into()))?,
id: self.id.unwrap_or("<unknown>".into()),
result: self
.result
.ok_or(NativeError::MissingInfo("result".into()))?,
log: self.log.ok_or(NativeError::MissingInfo("log".into()))?,
run_info: self
.run_info
.ok_or(NativeError::MissingInfo("run_info".into()))?,
log: self.log.unwrap_or("<unknown>".into()),
run_info: self.run_info.unwrap_or("<unknown>".into()),
timestamp: now,
})
}
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{LogFile, RunInfo};
const CSS: &str = include_str!("native-ci.css");
pub fn build_report(log: &mut LogFile, state: &Path) -> Result<(), ReportError> {
pub fn build_report(_log: &mut LogFile, state: &Path) -> Result<(), ReportError> {
let mut run_infos = collect_run_infos(state)?;
run_infos.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap());
Executable
+339
View File
@@ -0,0 +1,339 @@
#!/usr/bin/python3
import argparse
import json
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import yaml
class Suite:
def __init__(self, tmp):
self.tmp = tmp
self.rad = Rad(os.path.join(tmp, "dot-radicle"))
self.rad.auth()
self.config = Config(tmp)
self.config.write()
def _test_case(self, repo_name, shell):
git = Git(os.path.join(tmp, repo_name))
git.init()
git.write("README.md", "README")
native = NativeYaml(shell)
debug(f"native.yaml: {native.yaml()}")
git.write(".radicle/native.yaml", native.yaml())
git.commit("first commit")
commit = git.head()
self.rad.init(git)
rid = self.rad.rid(git)
trigger = Trigger(rid, commit)
return self._run_test_case(trigger)
def _run_test_case(self, trigger):
ci = NativeCI(self.rad, self.config)
exit, resps = ci.run(trigger)
debug(f"exit: {exit}")
debug(f"responses: {resps}")
return exit, resps
def assert_triggered(self, msg):
assert msg["response"] == "triggered"
def assert_success(self, msg):
assert msg == {"response": "finished", "result": "success"}
def assert_error(self, msg):
assert msg["response"] == "finished"
res = msg["result"]
assert isinstance(res, dict)
assert "error" in res
def run_all_test_cases(self, tests):
methods = self._test_methods()
chosen = self._chosen(methods, tests)
for meth in methods:
if meth in chosen:
info(f"test case {meth}")
getattr(self, meth)()
else:
info(f"skipping test {meth}")
def _test_methods(self):
return [meth for meth in dir(self) if meth.startswith("test_")]
def _chosen(self, methods, tests):
if tests:
chosen = set()
for test in tests:
for meth in methods:
if test in meth:
chosen.add(meth)
else:
chosen = set(methods)
return chosen
def test_happy_path(self):
exit, resps = self._test_case("happy-path", "echo hello, world")
assert exit == 0
assert len(resps) == 2
self.assert_triggered(resps[0])
self.assert_success(resps[1])
def test_command_fails(self):
exit, resps = self._test_case("cmd-fails", "false")
assert exit != 0
assert len(resps) == 2
self.assert_triggered(resps[0])
self.assert_error(resps[1])
def test_repository_does_not_exist(self):
trigger = Trigger(
"rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE",
"8d947e182b096ec009e1c9eda9e6a67f5eef83d9",
)
exit, resps = self._run_test_case(trigger)
assert exit != 0
assert len(resps) == 2
self.assert_triggered(resps[0])
self.assert_error(resps[1])
error = resps[1]["result"]["error"]
assert "git" in error
assert "clone" in error
def test_native_yaml_has_no_shell(self):
exit, resps = self._test_case("no-shell", None)
assert exit != 0
assert len(resps) == 1
self.assert_triggered(resps[0])
# FIXME: verify that stderr or build log contains complaint about shell
# missing
def test_native_yaml_shell_is_not_string(self):
exit, resps = self._test_case("shell-not-string", {"foo": "bar"})
assert exit != 0
assert len(resps) == 1
self.assert_triggered(resps[0])
# FIXME: verify that stderr or build log contains complaint about shell
# not being a string
class Git:
def __init__(self, path):
self.path = path
def filename(self, relative):
return os.path.join(self.path, "./" + relative)
def write(self, relative, data):
write(self.filename(relative), data)
def _git(self, argv):
return run(argv, cwd=self.path).stdout
def init(self):
assert not os.path.exists(self.path)
run(["git", "init", self.path])
debug(f"created git repository at {self.path}")
def commit(self, msg):
run(["git", "add", "."], cwd=self.path)
run(["git", "commit", "-m", msg], cwd=self.path)
def head(self):
return self._git(["git", "rev-parse", "HEAD"]).strip()
def ls_files(self):
out = self._git(["git", "ls-files"])
debug(f"ls-files:\n{out}")
class Rad:
PASSPHRASE = "xyzzy"
def __init__(self, rad_home):
assert rad_home is not None
self.rad_home = rad_home
def _rad(self, argv, env=None, cwd=None):
if env is None:
env = {}
env["RAD_HOME"] = self.rad_home
env["RAD_PASSPHRASE"] = self.PASSPHRASE
return run(argv, env=env, cwd=cwd).stdout
def auth(self):
self._rad(["rad", "auth", "--alias=test-node"])
debug(f"created Radicle node at {self.rad_home}")
def init(self, git):
name = os.path.basename(git.path)
self._rad(
[
"rad",
"init",
f"--name={name}",
f"--description=test-repo",
f"--public",
f"--no-confirm",
],
cwd=git.path,
)
debug(f"added git repository {git.path} to node")
def rid(self, git):
return self._rad(["rad", "."], cwd=git.path).strip()
class Config:
def __init__(self, tmp):
self.path = os.path.join(tmp, "config.yaml")
self.dict = {
"state": os.path.join(tmp, "state"),
"log": os.path.join(tmp, "node-log.txt"),
}
def write(self):
write(self.path, self.yaml())
def yaml(self):
return yaml.safe_dump(self.dict, indent=4)
class NativeYaml:
def __init__(self, shell):
self.dict = {}
if shell is not None:
self.dict["shell"] = shell
def yaml(self):
return yaml.safe_dump(self.dict, indent=4)
class Trigger:
def __init__(self, rid, commit):
self.rid = rid
self.commit = commit
def json(self):
return json.dumps(
{"request": "trigger", "repo": self.rid, "commit": self.commit}
)
class NativeCI:
def __init__(self, rad, config):
self.rad_home = rad.rad_home
self.config = config.path
def env(self):
return {
"RAD_HOME": self.rad_home,
"RADICLE_NATIVE_CI": self.config,
"RADICLE_NATIVE_CI_LOG": "debug",
}
def run(self, request):
p = run(
[
"cargo",
"run",
"-q",
],
input=request.json(),
env=self.env(),
may_fail=True,
)
return p.returncode, [
json.loads(line.strip()) for line in p.stdout.splitlines()
]
def debug(msg):
logging.debug(msg)
def info(msg):
logging.info(msg)
def die(msg, exc_info=None):
logging.critical(msg, exc_info=exc_info)
sys.exit(1)
def run(argv, env=None, cwd=None, input=None, may_fail=False):
debug(f"run {argv} with env={env}, cwd={cwd} input={input!r}")
if env is not None:
for name, value in os.environ.items():
if name not in env:
env[name] = value
p = subprocess.run(
argv, capture_output=True, env=env, cwd=cwd, input=input, text=True
)
debug(f"stdout:\n{indent(p.stdout)}")
debug(f"stderr:\n{indent(p.stderr)}")
if p.returncode != 0 and not may_fail:
die(f"command failed: {argv}")
return p
def indent(s):
return "".join([f" {line}\n" for line in s.splitlines()])
def write(filename, data):
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
with open(filename, "w") as f:
f.write(data)
def main(tmp, tests):
suite = Suite(tmp)
suite.run_all_test_cases(tests)
p = argparse.ArgumentParser()
p.add_argument("--verbose", action="store_true")
p.add_argument("--test", action="append", nargs="?", dest="tests")
args = p.parse_args()
if args.verbose:
level = logging.DEBUG
print(args)
else:
level = logging.INFO
logging.basicConfig(
level=level,
stream=sys.stderr,
format="%(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
tmp = tempfile.mkdtemp()
debug(f"created temporary directory {tmp}")
try:
main(tmp, args.tests)
except AssertionError as e:
debug(f"removing temporary directory {tmp}")
shutil.rmtree(tmp)
die(f"ERROR: assertion failed: {e}", exc_info=True)
except Exception as e:
debug(f"removing temporary directory {tmp}")
shutil.rmtree(tmp)
die(f"ERROR: uncaught exception {e}", exc_info=True)
else:
debug(f"removing temporary directory {tmp}")
shutil.rmtree(tmp)