From 5c7d2ea3ec1a0ef2aaf94a6d49dc0fc8600a2e93 Mon Sep 17 00:00:00 2001 From: jcamiel Date: Fri, 18 Nov 2022 15:29:16 +0100 Subject: [PATCH] Remove HTML ast for HTML report generation. --- packages/hurl/src/main.rs | 2 +- packages/hurl/src/report/html/ast.rs | 52 ---- packages/hurl/src/report/html/mod.rs | 239 +++++++++++++++++- packages/hurl/src/report/html/render.rs | 133 ---------- packages/hurl/src/report/mod.rs | 321 +----------------------- 5 files changed, 240 insertions(+), 507 deletions(-) delete mode 100644 packages/hurl/src/report/html/ast.rs delete mode 100644 packages/hurl/src/report/html/render.rs diff --git a/packages/hurl/src/main.rs b/packages/hurl/src/main.rs index 571b9c27d..4171bb1b9 100644 --- a/packages/hurl/src/main.rs +++ b/packages/hurl/src/main.rs @@ -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 { diff --git a/packages/hurl/src/report/html/ast.rs b/packages/hurl/src/report/html/ast.rs deleted file mode 100644 index 12fb6d75a..000000000 --- a/packages/hurl/src/report/html/ast.rs +++ /dev/null @@ -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, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Body { - pub children: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Element { - TextElement(String), - NodeElement { - name: String, - attributes: Vec, - children: Vec, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Attribute { - Class(String), - //Id(String), - Href(String), - Data(String, String), -} diff --git a/packages/hurl/src/report/html/mod.rs b/packages/hurl/src/report/html/mod.rs index efda75010..ebddd9a86 100644 --- a/packages/hurl/src/report/html/mod.rs +++ b/packages/hurl/src/report/html/mod.rs @@ -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::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, 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 { + let re = regex::Regex::new( + r#"(?x) + data-duration="(?P\d+)" + \s+ + data-status="(?P[a-z]+)" + \s+ + data-filename="(?P[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::>() +} + +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::>() + .join(""); + + format!( + r#" + + + Test Report + + + +

Test Report

+
+
{now}
+
Executed: {count_total} (100%)
+
Succeeded: {count_success} ({percentage_success})
+
Failed: {count_failure} ({percentage_failure})
+
+ + + + + + + +{rows} + +
FileStatusDuration
+ + +"#, + 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#" + {filename} + {status} + {duration_in_s} + +"#, + 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#" + +

Hurl Report

+ + + + + + + + + + + + + +
tests/hello.hurlsuccess0.1s
tests/failure.hurlfailure0.2s
+ + "#; + + 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, + } + ] + ); + } +} diff --git a/packages/hurl/src/report/html/render.rs b/packages/hurl/src/report/html/render.rs deleted file mode 100644 index 4e04bfa5c..000000000 --- a/packages/hurl/src/report/html/render.rs +++ /dev/null @@ -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!( - "\n{}{}", - self.head.render(), - self.body.render() - ) - } -} - -impl Head { - fn render(self) -> String { - let mut s = "".to_string(); - s.push_str(format!("{}", self.title).as_str()); - if let Some(filename) = self.stylesheet { - s.push_str( - format!( - "", - filename - ) - .as_str(), - ); - } - format!("{}", s) - } -} - -impl Body { - fn render(self) -> String { - let children: Vec = self.children.iter().map(|e| e.clone().render()).collect(); - format!("{}", 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::>() - .join(" ") - ) - }; - let children: Vec = 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(), "\nThis is a title

Hello world!

"); - } - - 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(), - "
".to_string() - ); - } -} diff --git a/packages/hurl/src/report/mod.rs b/packages/hurl/src/report/mod.rs index 76e50e3ef..7c5965e8a 100644 --- a/packages/hurl/src/report/mod.rs +++ b/packages/hurl/src/report/mod.rs @@ -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, 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 { - let re = regex::Regex::new( - r#"(?x) - data-duration="(?P\d+)" - \s+ - data-status="(?P[a-z]+)" - \s+ - data-filename="(?P[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::>() -} - -pub fn write_html_report(dir_path: PathBuf, hurl_results: Vec) -> 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::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) -> 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) -> 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#" - -

Hurl Report

-
- - - - - - - - - - - - -
tests/hello.hurlsuccess0.1s
tests/failure.hurlfailure0.2s
- - "#; - - 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, - } - ] - ); - } -}