Use an uuid as identifier for the HTML Hurl file run report

This commit is contained in:
jcamiel 2023-02-21 13:38:38 +01:00
parent 2a526d7b0b
commit 5939501adb
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
5 changed files with 55 additions and 42 deletions

11
Cargo.lock generated
View File

@ -522,6 +522,7 @@ dependencies = [
"sha2", "sha2",
"termion", "termion",
"url", "url",
"uuid",
"winres", "winres",
"xmltree", "xmltree",
] ]
@ -1193,6 +1194,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "uuid"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom",
"rand",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View File

@ -42,6 +42,8 @@ sha2 = "0.10.6"
url = "2.3.1" url = "2.3.1"
xmltree = { version = "0.10.3", features = ["attribute-order"] } xmltree = { version = "0.10.3", features = ["attribute-order"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
# uuid features: lets you generate random UUIDs and use a faster (but still sufficiently random) RNG
uuid = { version = "1.3.0", features = ["v4" , "fast-rng"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
termion = "2.0.1" termion = "2.0.1"

View File

@ -21,35 +21,42 @@
mod testcase; mod testcase;
use crate::report::Error; use crate::report::Error;
use crate::util::path;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
pub use testcase::Testcase; pub use testcase::Testcase;
/// The test result to be displayed in an HTML page /// The test result to be displayed in an HTML page
///
/// The filename has been [canonicalized] (https://doc.rust-lang.org/stable/std/path/struct.Path.html#method.canonicalize)
/// and does not need to exist in the filesystem
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
struct HTMLResult { struct HTMLResult {
/// Original filename, as given in the run execution
pub filename: String, pub filename: String,
/// The id of the corresponding [`Testcase`]
pub id: String,
pub time_in_ms: u128, pub time_in_ms: u128,
pub success: bool, pub success: bool,
} }
impl HTMLResult {
/// Creates a new HTMLResult from a [`Testcase`].
fn from(testcase: &Testcase) -> Self {
HTMLResult {
filename: testcase.filename.clone(),
id: testcase.id.clone(),
time_in_ms: testcase.time_in_ms,
success: testcase.success,
}
}
}
/// Creates and HTML report for this list of [`Testcase`] at `dir_path`/index.html. /// Creates and HTML report for this list of [`Testcase`] at `dir_path`/index.html.
/// ///
/// If the report already exists, results are merged. /// If the report already exists, results are merged.
pub fn write_report(dir_path: &Path, testcases: &[Testcase]) -> Result<(), Error> { pub fn write_report(dir_path: &Path, testcases: &[Testcase]) -> Result<(), Error> {
let index_path = dir_path.join("index.html"); let index_path = dir_path.join("index.html");
let mut results = parse_html(&index_path)?; let mut results = parse_html(&index_path)?;
for testcase in testcases { for testcase in testcases.iter() {
let html_result = HTMLResult { let html_result = HTMLResult::from(testcase);
filename: path::canonicalize_filename(&testcase.filename),
time_in_ms: testcase.time_in_ms,
success: testcase.success,
};
results.push(html_result); results.push(html_result);
} }
let now: DateTime<Local> = Local::now(); let now: DateTime<Local> = Local::now();
@ -111,19 +118,20 @@ fn parse_html_report(html: &str) -> Vec<HTMLResult> {
data-status="(?P<status>[a-z]+)" data-status="(?P<status>[a-z]+)"
\s+ \s+
data-filename="(?P<filename>[A-Za-z0-9_./-]+)" data-filename="(?P<filename>[A-Za-z0-9_./-]+)"
\s+
data-id="(?P<id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})"
"#, "#,
) )
.unwrap(); .unwrap();
re.captures_iter(html) re.captures_iter(html)
.map(|cap| { .map(|cap| {
let filename = cap["filename"].to_string(); let filename = cap["filename"].to_string();
// The HTML filename is using a relative path relatively in the report let id = cap["id"].to_string();
// to make the report portable
// But the original Hurl file is really an absolute file
let time_in_ms = cap["time_in_ms"].to_string().parse().unwrap(); let time_in_ms = cap["time_in_ms"].to_string().parse().unwrap();
let success = &cap["status"] == "success"; let success = &cap["status"] == "success";
HTMLResult { HTMLResult {
filename, filename,
id,
time_in_ms, time_in_ms,
success, success,
} }
@ -188,10 +196,16 @@ fn create_html_table_row(result: &HTMLResult) -> String {
let duration_in_ms = result.time_in_ms; let duration_in_ms = result.time_in_ms;
let duration_in_s = result.time_in_ms as f64 / 1000.0; let duration_in_s = result.time_in_ms as f64 / 1000.0;
let filename = &result.filename; let filename = &result.filename;
let displayed_filename = if filename == "-" {
"(standard input)"
} else {
filename
};
let id = &result.id;
format!( format!(
r#"<tr class="{status}" data-duration="{duration_in_ms}" data-status="{status}" data-filename="{filename}"> r#"<tr class="{status}" data-duration="{duration_in_ms}" data-status="{status}" data-filename="{filename}" data-id="{id}">
<td><a href="{filename}.html">{filename}</a></td> <td><a href="store/{id}.html">{displayed_filename}</a></td>
<td>{status}</td> <td>{status}</td>
<td>{duration_in_s}</td> <td>{duration_in_s}</td>
</tr> </tr>
@ -218,12 +232,12 @@ mod tests {
<h2>Hurl Report</h2> <h2>Hurl Report</h2>
<table> <table>
<tbody> <tbody>
<tr class="success" data-duration="100" data-status="success" data-filename="tests/hello.hurl"> <tr class="success" data-duration="100" data-status="success" data-filename="tests/hello.hurl" data-id="08aad14a-8d10-4ecc-892e-a72703c5b494">
<td><a href="tests/hello.hurl.html">tests/hello.hurl</a></td> <td><a href="tests/hello.hurl.html">tests/hello.hurl</a></td>
<td>success</td> <td>success</td>
<td>0.1s</td> <td>0.1s</td>
</tr> </tr>
<tr class="failure" data-duration="200" data-status="failure" data-filename="tests/failure.hurl"> <tr class="failure" data-duration="200" data-status="failure" data-filename="tests/failure.hurl" data-id="a6641ae3-8ce0-4d9f-80c5-3e23e032e055">
<td><a href="tests/failure.hurl.html">tests/failure.hurl</a></td> <td><a href="tests/failure.hurl.html">tests/failure.hurl</a></td>
<td>failure</td> <td>failure</td>
<td>0.2s</td> <td>0.2s</td>
@ -238,11 +252,13 @@ mod tests {
vec![ vec![
HTMLResult { HTMLResult {
filename: "tests/hello.hurl".to_string(), filename: "tests/hello.hurl".to_string(),
id: "08aad14a-8d10-4ecc-892e-a72703c5b494".to_string(),
time_in_ms: 100, time_in_ms: 100,
success: true, success: true,
}, },
HTMLResult { HTMLResult {
filename: "tests/failure.hurl".to_string(), filename: "tests/failure.hurl".to_string(),
id: "a6641ae3-8ce0-4d9f-80c5-3e23e032e055".to_string(),
time_in_ms: 200, time_in_ms: 200,
success: false, success: false,
} }

View File

@ -17,14 +17,14 @@
*/ */
use super::Error; use super::Error;
use crate::runner::HurlResult; use crate::runner::HurlResult;
use crate::util::path;
use hurl_core::parser; use hurl_core::parser;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Testcase { pub struct Testcase {
id: String, pub id: String,
pub filename: String, pub filename: String,
pub success: bool, pub success: bool,
pub time_in_ms: u128, pub time_in_ms: u128,
@ -33,8 +33,9 @@ pub struct Testcase {
impl Testcase { impl Testcase {
/// Creates an HTML testcase. /// Creates an HTML testcase.
pub fn from(hurl_result: &HurlResult) -> Testcase { pub fn from(hurl_result: &HurlResult) -> Testcase {
let id = Uuid::new_v4();
Testcase { Testcase {
id: "to-be-defined".to_string(), id: id.to_string(),
filename: hurl_result.filename.to_string(), filename: hurl_result.filename.to_string(),
time_in_ms: hurl_result.time_in_ms, time_in_ms: hurl_result.time_in_ms,
success: hurl_result.success, success: hurl_result.success,
@ -45,19 +46,14 @@ impl Testcase {
/// ///
/// For the moment, it's just an export of this HTML file, with syntax colored. /// For the moment, it's just an export of this HTML file, with syntax colored.
pub fn write_html(&self, content: &str, dir_path: &Path) -> Result<(), Error> { pub fn write_html(&self, content: &str, dir_path: &Path) -> Result<(), Error> {
let relative_input_file = path::canonicalize_filename(&self.filename); let output_file = dir_path.join("store").join(format!("{}.html", self.id));
let absolute_input_file = dir_path.join(format!("{relative_input_file}.html"));
let parent = absolute_input_file.parent().expect("a parent"); let parent = output_file.parent().expect("a parent");
std::fs::create_dir_all(parent).unwrap(); std::fs::create_dir_all(parent).unwrap();
let mut file = match std::fs::File::create(&absolute_input_file) { let mut file = match std::fs::File::create(&output_file) {
Err(why) => { Err(why) => {
return Err(Error { return Err(Error {
message: format!( message: format!("Issue writing to {}: {:?}", output_file.display(), why),
"Issue writing to {}: {:?}",
absolute_input_file.display(),
why
),
}); });
} }
Ok(file) => file, Ok(file) => file,
@ -68,11 +64,7 @@ impl Testcase {
if let Err(why) = file.write_all(s.as_bytes()) { if let Err(why) = file.write_all(s.as_bytes()) {
return Err(Error { return Err(Error {
message: format!( message: format!("Issue writing to {}: {:?}", output_file.display(), why),
"Issue writing to {}: {:?}",
absolute_input_file.display(),
why
),
}); });
} }
Ok(()) Ok(())

View File

@ -17,14 +17,6 @@
*/ */
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
/// Returns the canonical fullname relative to / (technically a relative path)
/// The function will panic if the input file does not exist
pub fn canonicalize_filename(input_file: &str) -> String {
let relative_input_file = Path::new(input_file).canonicalize().expect("existing file");
let relative_input_file = relative_input_file.to_string_lossy();
relative_input_file.trim_start_matches('/').to_string()
}
/// Return true if `path` is a descendant path of `ancestor`, false otherwise. /// Return true if `path` is a descendant path of `ancestor`, false otherwise.
pub fn is_descendant(path: &Path, ancestor: &Path) -> bool { pub fn is_descendant(path: &Path, ancestor: &Path) -> bool {
let path = normalize_path(path); let path = normalize_path(path);