Remove HTML ast for HTML report generation.

This commit is contained in:
jcamiel 2022-11-18 15:29:16 +01:00
parent 13eb44f264
commit 5c7d2ea3ec
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
5 changed files with 240 additions and 507 deletions

View File

@ -335,7 +335,7 @@ fn main() {
if let Some(dir_path) = cli_options.html_dir {
base_logger.debug(format!("Writing html report to {}", dir_path.display()).as_str());
let result = report::write_html_report(dir_path.clone(), hurl_results.clone());
let result = report::write_html_report(&dir_path, &hurl_results);
unwrap_or_exit(result, EXIT_ERROR_UNDEFINED, &base_logger);
for filename in filenames {

View File

@ -1,52 +0,0 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2022 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Html {
pub head: Head,
pub body: Body,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Head {
pub title: String,
pub stylesheet: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Body {
pub children: Vec<Element>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Element {
TextElement(String),
NodeElement {
name: String,
attributes: Vec<Attribute>,
children: Vec<Element>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Attribute {
Class(String),
//Id(String),
Href(String),
Data(String, String),
}

View File

@ -16,7 +16,240 @@
*
*/
pub use self::ast::{Attribute, Body, Element, Head, Html};
use crate::cli::CliError;
use crate::report::canonicalize_filename;
use crate::runner::HurlResult;
use chrono::{DateTime, Local};
use std::io::Write;
use std::path::Path;
mod ast;
mod render;
/// 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)]
struct HTMLResult {
pub filename: String,
pub time_in_ms: u128,
pub success: bool,
}
pub fn write_html_report(dir_path: &Path, hurl_results: &[HurlResult]) -> Result<(), CliError> {
let index_path = dir_path.join("index.html");
let mut results = parse_html(&index_path)?;
for result in hurl_results {
let html_result = HTMLResult {
filename: canonicalize_filename(&result.filename),
time_in_ms: result.time_in_ms,
success: result.success,
};
results.push(html_result);
}
let now: DateTime<Local> = Local::now();
let s = create_html_index(&now.to_rfc2822(), &results);
let file_path = dir_path.join("index.html");
let mut file = match std::fs::File::create(&file_path) {
Err(why) => {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
Ok(file) => file,
};
if let Err(why) = file.write_all(s.as_bytes()) {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
let file_path = dir_path.join("report.css");
let mut file = match std::fs::File::create(&file_path) {
Err(why) => {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
Ok(file) => file,
};
if let Err(why) = file.write_all(include_bytes!("../report.css")) {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
Ok(())
}
fn parse_html(path: &Path) -> Result<Vec<HTMLResult>, CliError> {
if path.exists() {
let s = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(why) => {
return Err(CliError {
message: format!("Issue reading {} to string to {:?}", path.display(), why),
});
}
};
Ok(parse_html_report(&s))
} else {
Ok(vec![])
}
}
fn parse_html_report(html: &str) -> Vec<HTMLResult> {
let re = regex::Regex::new(
r#"(?x)
data-duration="(?P<time_in_ms>\d+)"
\s+
data-status="(?P<status>[a-z]+)"
\s+
data-filename="(?P<filename>[A-Za-z0-9_./-]+)"
"#,
)
.unwrap();
re.captures_iter(html)
.map(|cap| {
let filename = cap["filename"].to_string();
// The HTML filename is using a relative path relatively in the report
// 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 success = &cap["status"] == "success";
HTMLResult {
filename,
time_in_ms,
success,
}
})
.collect::<Vec<HTMLResult>>()
}
fn percentage(count: usize, total: usize) -> String {
format!("{:.1}%", (count as f32 * 100.0) / total as f32)
}
fn create_html_index(now: &str, hurl_results: &[HTMLResult]) -> String {
let count_total = hurl_results.len();
let count_failure = hurl_results.iter().filter(|result| !result.success).count();
let count_success = hurl_results.iter().filter(|result| result.success).count();
let percentage_success = percentage(count_success, count_total);
let percentage_failure = percentage(count_failure, count_total);
let rows = hurl_results
.iter()
.map(create_html_table_row)
.collect::<Vec<String>>()
.join("");
format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>Test Report</title>
<link rel="stylesheet" type="text/css" href="report.css">
</head>
<body>
<h2>Test Report</h2>
<div class="summary">
<div class="date">{now}</div>
<div class="count">Executed: {count_total} (100%)</div>
<div class="count">Succeeded: {count_success} ({percentage_success})</div>
<div class="count">Failed: {count_failure} ({percentage_failure})</div>
</div>
<table>
<thead>
<td>File</td>
<td>Status</td>
<td>Duration</td>
</thead>
<tbody>
{rows}
</tbody>
</table>
</body>
</html>
"#,
now = now,
count_total = count_total,
count_success = count_success,
percentage_success = percentage_success,
count_failure = count_failure,
percentage_failure = percentage_failure
)
}
fn create_html_table_row(result: &HTMLResult) -> String {
let status = if result.success {
"success".to_string()
} else {
"failure".to_string()
};
let duration_in_ms = result.time_in_ms;
let duration_in_s = result.time_in_ms as f64 / 1000.0;
let filename = &result.filename;
format!(
r#"<tr class="{status}" data-duration="{duration_in_ms}" data-status="{status}" data-filename="{filename}">
<td><a href="{filename}.html">{filename}</a></td>
<td>{status}</td>
<td>{duration_in_s}</td>
</tr>
"#,
status = status,
duration_in_ms = duration_in_ms,
filename = filename,
duration_in_s = duration_in_s
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_percentage() {
assert_eq!(percentage(100, 100), "100.0%".to_string());
assert_eq!(percentage(66, 99), "66.7%".to_string());
assert_eq!(percentage(33, 99), "33.3%".to_string());
}
#[test]
fn test_parse_html_report() {
let html = r#"<html>
<body>
<h2>Hurl Report</h2>
<table>
<tbody>
<tr class="success" data-duration="100" data-status="success" data-filename="tests/hello.hurl">
<td><a href="tests/hello.hurl.html">tests/hello.hurl</a></td>
<td>success</td>
<td>0.1s</td>
</tr>
<tr class="failure" data-duration="200" data-status="failure" data-filename="tests/failure.hurl">
<td><a href="tests/failure.hurl.html">tests/failure.hurl</a></td>
<td>failure</td>
<td>0.2s</td>
</tr>
</tbody>
<table>
</body>
</html>"#;
assert_eq!(
parse_html_report(html),
vec![
HTMLResult {
filename: "tests/hello.hurl".to_string(),
time_in_ms: 100,
success: true,
},
HTMLResult {
filename: "tests/failure.hurl".to_string(),
time_in_ms: 200,
success: false,
}
]
);
}
}

View File

@ -1,133 +0,0 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2022 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use super::ast::*;
impl Html {
pub fn render(self) -> String {
format!(
"<!DOCTYPE html>\n<html>{}{}</html>",
self.head.render(),
self.body.render()
)
}
}
impl Head {
fn render(self) -> String {
let mut s = "".to_string();
s.push_str(format!("<title>{}</title>", self.title).as_str());
if let Some(filename) = self.stylesheet {
s.push_str(
format!(
"<link rel=\"stylesheet\" type=\"text/css\" href=\"{}\">",
filename
)
.as_str(),
);
}
format!("<head>{}</head>", s)
}
}
impl Body {
fn render(self) -> String {
let children: Vec<String> = self.children.iter().map(|e| e.clone().render()).collect();
format!("<body>{}</body>", children.join(""))
}
}
impl Element {
fn render(self) -> String {
match self {
Element::NodeElement {
name,
children,
attributes,
} => {
let attributes = if attributes.is_empty() {
"".to_string()
} else {
format!(
" {}",
attributes
.iter()
.map(|a| a.clone().render())
.collect::<Vec<String>>()
.join(" ")
)
};
let children: Vec<String> = children.iter().map(|e| e.clone().render()).collect();
format!("<{}{}>{}</{}>", name, attributes, children.join(""), name)
}
Element::TextElement(s) => s,
}
}
}
impl Attribute {
fn render(self) -> String {
match self {
Attribute::Class(s) => format!("class=\"{}\"", s),
//Attribute::Id(s) => format!("id=\"{}\"", s),
Attribute::Href(s) => format!("href=\"{}\"", s),
Attribute::Data(name, value) => format!("data-{}=\"{}\"", name, value),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
pub fn sample_html() -> Html {
Html {
head: Head {
title: "This is a title".to_string(),
stylesheet: None,
},
body: Body {
children: vec![Element::NodeElement {
name: "p".to_string(),
attributes: vec![],
children: vec![Element::TextElement("Hello world!".to_string())],
}],
},
}
}
#[test]
fn test_render_html() {
assert_eq!(sample_html().render(), "<!DOCTYPE html>\n<html><head><title>This is a title</title></head><body><p>Hello world!</p></body></html>");
}
pub fn sample_div() -> Element {
Element::NodeElement {
name: "div".to_string(),
attributes: vec![Attribute::Class("request".to_string())],
children: vec![],
}
}
#[test]
fn test_render_div() {
assert_eq!(
sample_div().render(),
"<div class=\"request\"></div>".to_string()
);
}
}

View File

@ -15,333 +15,18 @@
* limitations under the License.
*
*/
use chrono::{DateTime, Local};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use super::cli::CliError;
use super::runner::HurlResult;
mod html;
mod junit;
pub use html::write_html_report;
pub use junit::create_report as create_junit_report;
pub use junit::Testcase;
use std::path::Path;
/// 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)]
pub struct HTMLResult {
pub filename: String,
pub time_in_ms: u128,
pub success: bool,
}
pub fn parse_html(path: PathBuf) -> Result<Vec<HTMLResult>, CliError> {
if path.exists() {
let s = match std::fs::read_to_string(path.clone()) {
Ok(s) => s,
Err(why) => {
return Err(CliError {
message: format!("Issue reading {} to string to {:?}", path.display(), why),
});
}
};
Ok(parse_html_report(s.as_str()))
} else {
Ok(vec![])
}
}
fn parse_html_report(html: &str) -> Vec<HTMLResult> {
let re = regex::Regex::new(
r#"(?x)
data-duration="(?P<time_in_ms>\d+)"
\s+
data-status="(?P<status>[a-z]+)"
\s+
data-filename="(?P<filename>[A-Za-z0-9_./-]+)"
"#,
)
.unwrap();
re.captures_iter(html)
.map(|cap| {
let filename = cap["filename"].to_string();
// The HTML filename is using a relative path relatively in the report
// 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 success = &cap["status"] == "success";
HTMLResult {
filename,
time_in_ms,
success,
}
})
.collect::<Vec<HTMLResult>>()
}
pub fn write_html_report(dir_path: PathBuf, hurl_results: Vec<HurlResult>) -> Result<(), CliError> {
let index_path = dir_path.join("index.html");
let mut results = parse_html(index_path)?;
for result in hurl_results {
let html_result = HTMLResult {
filename: canonicalize_filename(&result.filename),
time_in_ms: result.time_in_ms,
success: result.success,
};
results.push(html_result);
}
let now: DateTime<Local> = Local::now();
let html = create_html_index(now.to_rfc2822(), results);
let s = html.render();
let file_path = dir_path.join("index.html");
let mut file = match std::fs::File::create(&file_path) {
Err(why) => {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
Ok(file) => file,
};
if let Err(why) = file.write_all(s.as_bytes()) {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
let file_path = dir_path.join("report.css");
let mut file = match std::fs::File::create(&file_path) {
Err(why) => {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
Ok(file) => file,
};
if let Err(why) = file.write_all(include_bytes!("report.css")) {
return Err(CliError {
message: format!("Issue writing to {}: {:?}", file_path.display(), why),
});
}
Ok(())
}
fn percentage(count: usize, total: usize) -> String {
format!("{:.1}%", (count as f32 * 100.0) / total as f32)
}
fn create_html_index(now: String, hurl_results: Vec<HTMLResult>) -> html::Html {
let head = html::Head {
title: "Test Report".to_string(),
stylesheet: Some("report.css".to_string()),
};
let count_total = hurl_results.len();
let count_failure = hurl_results.iter().filter(|result| !result.success).count();
let count_success = hurl_results.iter().filter(|result| result.success).count();
let body = html::Body {
children: vec![
html::Element::NodeElement {
name: "h2".to_string(),
attributes: vec![],
children: vec![html::Element::TextElement("Test Report".to_string())],
},
html::Element::NodeElement {
name: "div".to_string(),
attributes: vec![html::Attribute::Class("summary".to_string())],
children: vec![
html::Element::NodeElement {
name: "div".to_string(),
attributes: vec![html::Attribute::Class("date".to_string())],
children: vec![html::Element::TextElement(now)],
},
html::Element::NodeElement {
name: "div".to_string(),
attributes: vec![html::Attribute::Class("count".to_string())],
children: vec![html::Element::TextElement(format!(
"Executed: {} (100%)",
count_total
))],
},
html::Element::NodeElement {
name: "div".to_string(),
attributes: vec![html::Attribute::Class("count".to_string())],
children: vec![html::Element::TextElement(format!(
"Succeeded: {} ({})",
count_success,
percentage(count_success, count_total)
))],
},
html::Element::NodeElement {
name: "div".to_string(),
attributes: vec![html::Attribute::Class("count".to_string())],
children: vec![html::Element::TextElement(format!(
"Failed: {} ({})",
count_failure,
percentage(count_failure, count_total)
))],
},
],
},
html::Element::NodeElement {
name: "table".to_string(),
attributes: vec![],
children: vec![
create_html_table_header(),
create_html_table_body(hurl_results),
],
},
],
};
html::Html { head, body }
}
fn create_html_table_header() -> html::Element {
html::Element::NodeElement {
name: "thead".to_string(),
attributes: vec![],
children: vec![html::Element::NodeElement {
name: "tr".to_string(),
attributes: vec![],
children: vec![
html::Element::NodeElement {
name: "td".to_string(),
attributes: vec![],
children: vec![html::Element::TextElement("filename".to_string())],
},
html::Element::NodeElement {
name: "td".to_string(),
attributes: vec![],
children: vec![html::Element::TextElement("status".to_string())],
},
html::Element::NodeElement {
name: "td".to_string(),
attributes: vec![],
children: vec![html::Element::TextElement("duration".to_string())],
},
],
}],
}
}
fn create_html_table_body(hurl_results: Vec<HTMLResult>) -> html::Element {
let children = hurl_results
.iter()
.map(|result| create_html_result(result.clone()))
.collect();
html::Element::NodeElement {
name: "tbody".to_string(),
attributes: vec![],
children,
}
}
///
/// return the canonical fullname relative to / (technically a relative path)
/// 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()
}
fn create_html_result(result: HTMLResult) -> html::Element {
let status = if result.success {
"success".to_string()
} else {
"failure".to_string()
};
html::Element::NodeElement {
name: "tr".to_string(),
attributes: vec![
html::Attribute::Class(status.clone()),
html::Attribute::Data("duration".to_string(), result.time_in_ms.to_string()),
html::Attribute::Data("status".to_string(), status.clone()),
html::Attribute::Data("filename".to_string(), result.filename.clone()),
],
children: vec![
html::Element::NodeElement {
name: "td".to_string(),
attributes: vec![],
children: vec![html::Element::NodeElement {
name: "a".to_string(),
attributes: vec![html::Attribute::Href(format!("{}.html", result.filename))],
children: vec![html::Element::TextElement(result.filename)],
}],
},
html::Element::NodeElement {
name: "td".to_string(),
attributes: vec![],
children: vec![html::Element::TextElement(status)],
},
html::Element::NodeElement {
name: "td".to_string(),
attributes: vec![],
children: vec![html::Element::TextElement(format!(
"{}s",
result.time_in_ms as f64 / 1000.0
))],
},
],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_percentage() {
assert_eq!(percentage(100, 100), "100.0%".to_string());
assert_eq!(percentage(66, 99), "66.7%".to_string());
assert_eq!(percentage(33, 99), "33.3%".to_string());
}
#[test]
fn test_parse_html_report() {
let html = r#"<html>
<body>
<h2>Hurl Report</h2>
<table>
<tbody>
<tr class="success" data-duration="100" data-status="success" data-filename="tests/hello.hurl">
<td><a href="tests/hello.hurl.html">tests/hello.hurl</a></td>
<td>success</td>
<td>0.1s</td>
</tr>
<tr class="failure" data-duration="200" data-status="failure" data-filename="tests/failure.hurl">
<td><a href="tests/failure.hurl.html">tests/failure.hurl</a></td>
<td>failure</td>
<td>0.2s</td>
</tr>
</tbody>
<table>
</body>
</html>"#;
assert_eq!(
parse_html_report(html),
vec![
HTMLResult {
filename: "tests/hello.hurl".to_string(),
time_in_ms: 100,
success: true,
},
HTMLResult {
filename: "tests/failure.hurl".to_string(),
time_in_ms: 200,
success: false,
}
]
);
}
}