summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2024-01-12 11:52:12 +0200
committerLars Wirzenius <liw@liw.fi>2024-01-12 13:12:44 +0200
commitaafb25125403dfafce69c4db699f38d244d8cf9e (patch)
tree0b15fe288eec51f630c9529d23c7da6fea3bddc6
parentcced88081324cfdea50bc5f32efea1f0fe58a3b4 (diff)
downloadradicle-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.rs18
-rw-r--r--src/bin/run_log.rs22
-rw-r--r--src/lib.rs1
-rw-r--r--src/runcmd.rs20
-rw-r--r--src/runlog.rs159
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");
+}
diff --git a/src/lib.rs b/src/lib.rs
index 55d836b..84a1a47 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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),
+}