diff options
author | Lars Wirzenius <liw@liw.fi> | 2024-01-12 11:52:12 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2024-01-12 13:12:44 +0200 |
commit | aafb25125403dfafce69c4db699f38d244d8cf9e (patch) | |
tree | 0b15fe288eec51f630c9529d23c7da6fea3bddc6 | |
parent | cced88081324cfdea50bc5f32efea1f0fe58a3b4 (diff) | |
download | radicle-native-ci-aafb25125403dfafce69c4db699f38d244d8cf9e.tar.gz |
feat: add a module to construct HTML run logs
Signed-off-by: Lars Wirzenius <liw@liw.fi>
-rw-r--r-- | src/bin/radicle-native-ci.rs | 18 | ||||
-rw-r--r-- | src/bin/run_log.rs | 22 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/runcmd.rs | 20 | ||||
-rw-r--r-- | src/runlog.rs | 159 |
5 files changed, 201 insertions, 19 deletions
diff --git a/src/bin/radicle-native-ci.rs b/src/bin/radicle-native-ci.rs index d2bf5fe..9bd12d6 100644 --- a/src/bin/radicle-native-ci.rs +++ b/src/bin/radicle-native-ci.rs @@ -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"))?, @@ -351,5 +348,8 @@ enum NativeError { RunInfo(#[from] RunInfoError), #[error(transparent)] + RunLog(#[from] RunLogError), + + #[error(transparent)] RunSpec(#[from] RunSpecError), } diff --git a/src/bin/run_log.rs b/src/bin/run_log.rs new file mode 100644 index 0000000..0f226ac --- /dev/null +++ b/src/bin/run_log.rs @@ -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"); +} @@ -4,4 +4,5 @@ pub mod msg; pub mod report; pub mod runcmd; pub mod runinfo; +pub mod runlog; pub mod runspec; diff --git a/src/runcmd.rs b/src/runcmd.rs index d1c8e5b..6010259 100644 --- a/src/runcmd.rs +++ b/src/runcmd.rs @@ -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), diff --git a/src/runlog.rs b/src/runlog.rs new file mode 100644 index 0000000..cf47f93 --- /dev/null +++ b/src/runlog.rs @@ -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), +} |