feat: add a module to construct HTML run logs

Signed-off-by: Lars Wirzenius <liw@liw.fi>
This commit is contained in:
Lars Wirzenius
2024-01-12 13:12:44 +02:00
parent cced880813
commit aafb251254
5 changed files with 201 additions and 19 deletions
+9 -9
View File
@@ -30,6 +30,7 @@ use radicle_native_ci::{
report,
runcmd::{runcmd, RunCmdError},
runinfo::{RunInfo, RunInfoBuilder, RunInfoError},
runlog::{RunLog, RunLogError},
runspec::{RunSpec, RunSpecError},
};
@@ -155,7 +156,7 @@ struct Runner<'a> {
commit: Oid,
src: PathBuf,
log: &'a mut LogFile,
run_log: LogFile,
run_log: RunLog,
timeout: Option<usize>,
builder: &'a mut RunInfoBuilder,
}
@@ -193,11 +194,9 @@ impl<'a> Runner<'a> {
self.log
.writeln(&format!("CI run on {}, {}", self.repo, self.commit))?;
self.run_log.h1("Log from Radicle native CI")?;
self.run_log
.bullet_point(format!("* Repository id: `{}`", self.repo))?;
self.run_log
.bullet_point(format!("* Commit: `{}", self.commit))?;
self.run_log.title("Log from Radicle native CI");
self.run_log.rid(self.repo);
self.run_log.commit(self.commit);
write_triggered(&self.run_id)?;
@@ -228,8 +227,6 @@ impl<'a> Runner<'a> {
write_response(&Response::finished(result.clone()))?;
self.run_log.writeln("CI run finished successfully")?;
std::fs::remove_dir_all(&self.src)
.map_err(|e| NativeError::RemoveDir(self.src.clone(), e))?;
@@ -300,7 +297,7 @@ impl<'a> RunnerBuilder<'a> {
fn build(self) -> Result<Runner<'a>, NativeError> {
let run_log = self.run_log.ok_or(NativeError::Unset("run_log"))?;
let run_log = LogFile::open(&run_log)?;
let run_log = RunLog::new(&run_log);
Ok(Runner {
run_id: self.run_id.ok_or(NativeError::Unset("run_id"))?,
storage: self.storage.ok_or(NativeError::Unset("storage"))?,
@@ -350,6 +347,9 @@ enum NativeError {
#[error(transparent)]
RunInfo(#[from] RunInfoError),
#[error(transparent)]
RunLog(#[from] RunLogError),
#[error(transparent)]
RunSpec(#[from] RunSpecError),
}
+22
View File
@@ -0,0 +1,22 @@
use std::path::Path;
use radicle_ci_broker::msg::{Id, Oid};
use radicle_native_ci::runlog::RunLog;
/// The main program.
fn main() {
let mut run_log = RunLog::new(Path::new("testlog.html"));
run_log.title("Some Title");
run_log.rid(Id::from_urn("rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE").expect("rid"));
run_log.commit(Oid::try_from("b788f7ffd38572614457adb1656c0b4575b941dd").expect("commit"));
run_log.runcmd(
&["git", "pull"],
Path::new("/tmp"),
0,
"This is stdout".as_bytes(),
"Error messages go here".as_bytes(),
);
run_log.write().expect("write html log");
}
+1
View File
@@ -4,4 +4,5 @@ pub mod msg;
pub mod report;
pub mod runcmd;
pub mod runinfo;
pub mod runlog;
pub mod runspec;
+10 -10
View File
@@ -4,17 +4,13 @@ use log::{debug, error};
use radicle_ci_broker::msg::{MessageError, Response};
use crate::logfile::{LogError, LogFile};
use crate::runlog::{RunLog, RunLogError};
/// Run a command in a directory.
pub fn runcmd(log: &mut LogFile, argv: &[&str], cwd: &Path) -> Result<(), RunCmdError> {
pub fn runcmd(run_log: &mut RunLog, argv: &[&str], cwd: &Path) -> Result<(), RunCmdError> {
debug!("runcmd: argv={:?}", argv);
debug!("runcmd: cwd={:?}", cwd);
log.h2("Run command")?;
log.fenced("argv", format!("{:?}", argv).as_bytes())?;
log.writeln(&format!("in directory: {}\n", cwd.display()))?;
assert!(!argv.is_empty());
let argv0 = argv[0];
let output = Command::new(argv0)
@@ -26,10 +22,14 @@ pub fn runcmd(log: &mut LogFile, argv: &[&str], cwd: &Path) -> Result<(), RunCmd
let exit = output.status;
debug!("exit: {:?}", exit);
log.writeln(&format!("Exit: {}\n\n", exit.code().unwrap()))?;
log.fenced("Standard output", &output.stdout)?;
log.fenced("Standard error", &output.stderr)?;
run_log.runcmd(
argv,
cwd,
exit.code().unwrap(),
&output.stdout,
&output.stderr,
);
if !exit.success() {
let error = Response::error(&format!("command failed: {:?}", argv));
@@ -50,7 +50,7 @@ pub enum RunCmdError {
WriteResponse(Response, #[source] MessageError),
#[error(transparent)]
Log(#[from] LogError),
RunLog(#[from] RunLogError),
#[error("failed to run command {0:?}")]
Command(Vec<String>, #[source] std::io::Error),
+159
View File
@@ -0,0 +1,159 @@
use std::path::{Path, PathBuf};
use html_page::{Document, Element, Tag};
use radicle_ci_broker::msg::{Id, Oid};
#[derive(Debug, Default)]
pub struct RunLog {
filename: PathBuf,
title: Option<String>,
rid: Option<Id>,
commit: Option<Oid>,
commands: Vec<Command>,
}
impl RunLog {
pub fn new(filename: &Path) -> Self {
Self {
filename: filename.into(),
..Default::default()
}
}
pub fn title(&mut self, title: &str) {
self.title = Some(title.into());
}
pub fn rid(&mut self, rid: Id) {
self.rid = Some(rid);
}
pub fn commit(&mut self, commit: Oid) {
self.commit = Some(commit);
}
pub fn runcmd(&mut self, argv: &[&str], cwd: &Path, exit: i32, stdout: &[u8], stderr: &[u8]) {
self.commands.push(Command {
argv: argv.iter().map(|a| a.to_string()).collect(),
cwd: cwd.into(),
exit,
stdout: stdout.to_vec(),
stderr: stderr.to_vec(),
});
}
pub fn write(&self) -> Result<(), RunLogError> {
let title = self.title.as_ref().ok_or(RunLogError::Missing("title"))?;
let rid = self.rid.as_ref().ok_or(RunLogError::Missing("rid"))?;
let commit = self.commit.as_ref().ok_or(RunLogError::Missing("commit"))?;
let mut doc = Document::default();
doc.push_to_head(&Element::new(Tag::Title).with_text(title));
doc.push_to_body(&Element::new(Tag::H1).with_text(title));
let mut ul = Element::new(Tag::Ul);
ul.push_child(
&Element::new(Tag::Li)
.with_text("Repository id: ")
.with_child(Element::new(Tag::Code).with_text(&rid.to_string())),
);
ul.push_child(
&Element::new(Tag::Li)
.with_text("Commit: ")
.with_child(Element::new(Tag::Code).with_text(&commit.to_string())),
);
ul.push_child(
&Element::new(Tag::Li)
.with_text("Result: ")
.with_child(Element::new(Tag::B).with_text(self.result())),
);
doc.push_to_body(&ul);
for cmd in self.commands.iter() {
cmd.format(&mut doc);
}
let html = format!("{}", doc);
std::fs::write(&self.filename, html.as_bytes())
.map_err(|e| RunLogError::Write(self.filename.clone(), e))?;
Ok(())
}
fn result(&self) -> &'static str {
if self.commands.iter().any(|c| c.exit != 0) {
"FAILURE"
} else {
"SUCCESS"
}
}
}
#[derive(Debug)]
struct Command {
argv: Vec<String>,
cwd: PathBuf,
exit: i32,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
impl Command {
fn command(&self) -> String {
self.argv.as_slice().join(" ")
}
fn format(&self, doc: &mut Document) {
doc.push_to_body(
&Element::new(Tag::H2)
.with_text("Run: ")
.with_child(Element::new(Tag::Code).with_text(&self.command())),
);
doc.push_to_body(&Element::new(Tag::P).with_text("Command arguments:"));
let mut ul = Element::new(Tag::Ul);
for arg in self.argv.iter() {
ul.push_child(
&Element::new(Tag::Li)
.with_child(Element::new(Tag::Code).with_text(&format!("{:?}", arg))),
);
}
doc.push_to_body(&ul);
doc.push_to_body(
&Element::new(Tag::P)
.with_text("In directory: ")
.with_child(Element::new(Tag::Code).with_text(&format!("{}", self.cwd.display()))),
);
doc.push_to_body(&Element::new(Tag::P).with_text(&format!("Exit code: {}", self.exit)));
self.output(doc, "Stdout:", &self.stdout);
self.output(doc, "Stderr:", &self.stderr);
}
fn output(&self, doc: &mut Document, stream: &str, data: &[u8]) {
if !data.is_empty() {
doc.push_to_body(&Element::new(Tag::P).with_text(stream));
doc.push_to_body(
&Element::new(Tag::P).with_child(
Element::new(Tag::Blockquote).with_child(
Element::new(Tag::Pre).with_text(&String::from_utf8_lossy(data)),
),
),
);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RunLogError {
#[error("programming error: missing field for run log {0}")]
Missing(&'static str),
#[error("failed to write HTML run log to {0}")]
Write(PathBuf, #[source] std::io::Error),
}