mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-26 11:43:08 +03:00
Remove HTML ast for HTML report generation.
This commit is contained in:
parent
13eb44f264
commit
5c7d2ea3ec
@ -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 {
|
||||
|
@ -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),
|
||||
}
|
@ -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,
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user