mirror of
https://seed.radicle.dev/rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE.git
synced 2026-11-10 21:50:43 +08:00
feat: add a module to construct HTML run logs
Signed-off-by: Lars Wirzenius <liw@liw.fi>
This commit is contained in:
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
-10
@@ -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
@@ -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),
|
||||
}
|
||||
Reference in New Issue
Block a user