diff --git a/packages/hurl/src/report/html/testcase.rs b/packages/hurl/src/report/html/testcase.rs index bfbe06298..a17111cf9 100644 --- a/packages/hurl/src/report/html/testcase.rs +++ b/packages/hurl/src/report/html/testcase.rs @@ -36,16 +36,13 @@ impl Testcase { /// Creates an HTML testcase. pub fn from(hurl_result: &HurlResult, filename: &str) -> Testcase { let id = Uuid::new_v4(); + let errors = hurl_result.errors().into_iter().cloned().collect(); Testcase { id: id.to_string(), filename: filename.to_string(), time_in_ms: hurl_result.time_in_ms, success: hurl_result.success, - errors: hurl_result - .entries - .iter() - .flat_map(|e| e.errors.clone()) - .collect(), + errors, } } diff --git a/packages/hurl/src/report/html/timeline/calls.rs b/packages/hurl/src/report/html/timeline/calls.rs index 3c8e5c615..69eddb5a2 100644 --- a/packages/hurl/src/report/html/timeline/calls.rs +++ b/packages/hurl/src/report/html/timeline/calls.rs @@ -21,8 +21,10 @@ use crate::report::html::timeline::svg::Attribute::{ }; use crate::report::html::timeline::svg::{Element, ElementKind}; use crate::report::html::timeline::unit::{Pixel, Px}; -use crate::report::html::timeline::util::{new_failure_icon, new_success_icon, trunc_str}; -use crate::report::html::timeline::{svg, CallContext, CALL_HEIGHT}; +use crate::report::html::timeline::util::{ + new_failure_icon, new_retry_icon, new_success_icon, trunc_str, +}; +use crate::report::html::timeline::{svg, CallContext, CallContextKind, CALL_HEIGHT}; use crate::report::html::Testcase; use std::iter::zip; @@ -47,6 +49,8 @@ impl Testcase { root.add_child(symbol); let symbol = new_failure_icon("failure"); root.add_child(symbol); + let symbol = new_retry_icon("retry"); + root.add_child(symbol); // Add a flat background. let mut elt = Element::new(ElementKind::Rect); @@ -57,15 +61,17 @@ impl Testcase { elt.add_attr(Fill("#fbfafd".to_string())); root.add_child(elt); - // Add horizontal lines - let x = 0.px(); - let y = margin_top; - let elt = new_grid(calls, y, width, height); - root.add_child(elt); + if !calls.is_empty() { + // Add horizontal lines + let x = 0.px(); + let y = margin_top; + let elt = new_grid(calls, y, width, height); + root.add_child(elt); - // Add calls info - let elt = new_calls(calls, call_ctxs, x, y); - root.add_child(elt); + // Add calls info + let elt = new_calls(calls, call_ctxs, x, y); + root.add_child(elt); + } root.to_string() } @@ -96,10 +102,10 @@ fn new_calls( // Icon success / failure let mut elt = svg::new_use(); - let icon = if call_ctx.success { - "#success" - } else { - "#failure" + let icon = match call_ctx.kind { + CallContextKind::Success => "#success", + CallContextKind::Failure => "#failure", + CallContextKind::Retry => "#retry", }; elt.add_attr(Href(icon.to_string())); elt.add_attr(X(x.0 - 6.0)); @@ -117,7 +123,7 @@ fn new_calls( let text = format!("{} {url}", call.request.method); let text = trunc_str(&text, 24); let mut elt = svg::new_text(x.0, y.0, &text); - if !call_ctx.success { + if call_ctx.kind == CallContextKind::Failure { elt.add_attr(Fill("red".to_string())); } group.add_child(elt); @@ -126,7 +132,7 @@ fn new_calls( x += 180.px(); let text = format!("{}", call.response.status); let mut elt = svg::new_text(x.0, y.0, &text); - if !call_ctx.success { + if call_ctx.kind == CallContextKind::Failure { elt.add_attr(Fill("red".to_string())); } group.add_child(elt); diff --git a/packages/hurl/src/report/html/timeline/mod.rs b/packages/hurl/src/report/html/timeline/mod.rs index 80f539cf9..675f6e56b 100644 --- a/packages/hurl/src/report/html/timeline/mod.rs +++ b/packages/hurl/src/report/html/timeline/mod.rs @@ -33,14 +33,20 @@ mod waterfall; const CALL_HEIGHT: Pixel = Pixel(24.0); const CALL_INSET: Pixel = Pixel(3.0); +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum CallContextKind { + Success, // call context parent entry is successful + Failure, // call context parent entry is in error and has not been retried + Retry, // call context parent entry is in error and has been retried +} /// A structure that holds information to construct a SVG view /// of a [`Call`] pub struct CallContext { - pub success: bool, // If the parent entry is successful or not - pub line: usize, // Line number of the source entry (1-based) - pub entry_index: usize, // Index of the runtime EntryResult + pub kind: CallContextKind, // If the parent entry is successful, retried or in error. + pub line: usize, // Line number of the source entry (1-based) + pub entry_index: usize, // Index of the runtime EntryResult pub call_entry_index: usize, // Index of the runtime Call in the current entry - pub call_index: usize, // Index of the runtime Call in the whole run + pub call_index: usize, // Index of the runtime Call in the whole run pub source_filename: String, pub run_filename: String, } @@ -80,13 +86,24 @@ impl Testcase { /// Constructs a list of call contexts to record source line code, runtime entry and call indices. fn get_call_contexts(&self, hurl_file: &HurlFile, entries: &[EntryResult]) -> Vec { let mut calls_ctx = vec![]; + for (entry_index, e) in entries.iter().enumerate() { + let next_e = entries.get(entry_index + 1); + let retry = match next_e { + None => false, // last entry of the whole run can't be retried + Some(next_e) => e.entry_index == next_e.entry_index, + }; + let kind = match (e.errors.is_empty(), retry) { + (true, _) => CallContextKind::Success, + (false, true) => CallContextKind::Retry, + (false, false) => CallContextKind::Failure, + }; for (call_entry_index, _) in e.calls.iter().enumerate() { let entry_src_index = e.entry_index - 1; let entry_src = hurl_file.entries.get(entry_src_index).unwrap(); let line = entry_src.request.space0.source_info.start.line; let ctx = CallContext { - success: e.errors.is_empty(), + kind, line, entry_index: entry_index + 1, call_entry_index: call_entry_index + 1, diff --git a/packages/hurl/src/report/html/timeline/util.rs b/packages/hurl/src/report/html/timeline/util.rs index c4992b322..da2b6eddb 100644 --- a/packages/hurl/src/report/html/timeline/util.rs +++ b/packages/hurl/src/report/html/timeline/util.rs @@ -68,6 +68,11 @@ pub fn new_failure_icon(id: &str) -> Element { new_icon(id, 512.px(), 512.px(), "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z", "red") } +/// Returns the SVG retry icon identified by id. +pub fn new_retry_icon(id: &str) -> Element { + new_icon(id, 512.px(), 512.px(), "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z", "gold") +} + /// Returns a SVG icon identified by `id`, with a `width` pixel by `height` pixel size, `path` and `color`. fn new_icon(id: &str, width: Pixel, height: Pixel, path: &str, color: &str) -> Element { let mut symbol = svg::new_symbol(); diff --git a/packages/hurl/src/report/html/timeline/waterfall.rs b/packages/hurl/src/report/html/timeline/waterfall.rs index dcc696521..b1805a711 100644 --- a/packages/hurl/src/report/html/timeline/waterfall.rs +++ b/packages/hurl/src/report/html/timeline/waterfall.rs @@ -30,9 +30,9 @@ use crate::report::html::timeline::unit::{ Byte, Interval, Microsecond, Millisecond, Pixel, Px, Scale, Second, TimeUnit, }; use crate::report::html::timeline::util::{ - new_failure_icon, new_stripes, new_success_icon, trunc_str, + new_failure_icon, new_retry_icon, new_stripes, new_success_icon, trunc_str, }; -use crate::report::html::timeline::{svg, CallContext, CALL_HEIGHT, CALL_INSET}; +use crate::report::html::timeline::{svg, CallContext, CallContextKind, CALL_HEIGHT, CALL_INSET}; use crate::report::html::Testcase; /// Returns the start and end date for these entries. @@ -79,6 +79,8 @@ impl Testcase { root.add_child(elt); let elt = new_failure_icon("failure"); root.add_child(elt); + let elt = new_retry_icon("retry"); + root.add_child(elt); // We add some space for the right last grid labels. let pixels_x = Interval::new(0.px(), width); @@ -335,10 +337,10 @@ fn new_call_tooltip( // Icon + URL + method let mut elt = svg::new_use(); - let icon = if call_ctx.success { - "#success" - } else { - "#failure" + let icon = match call_ctx.kind { + CallContextKind::Success => "#success", + CallContextKind::Failure => "#failure", + CallContextKind::Retry => "#retry", }; elt.add_attr(Href(icon.to_string())); elt.add_attr(X(x.0)); @@ -351,7 +353,10 @@ fn new_call_tooltip( let text = trunc_str(&text, 54); let text = format!("{text} {}", call.response.status); let mut elt = svg::new_text(x.0 + 30.0, y.0 + 16.0, &text); - let color = if call_ctx.success { "#555" } else { "red" }; + let color = match call_ctx.kind { + CallContextKind::Success | CallContextKind::Retry => "#555", + CallContextKind::Failure => "red", + }; elt.add_attr(Fill(color.to_string())); elt.add_attr(FontWeight("bold".to_string())); group.add_child(elt); @@ -436,30 +441,40 @@ fn new_call_tooltip( // Run URL y += 56.px(); - let run = format!( + let href = format!( "{}#e{}:c{}", call_ctx.run_filename, call_ctx.entry_index, call_ctx.call_entry_index ); - let mut elt = svg::new_text(x.0, y.0, "(view run)"); - elt.add_attr(Fill("royalblue".to_string())); - elt.add_attr(TextDecoration("underline".to_string())); - let mut a = new_a(&run); - a.add_child(elt); - group.add_child(a); + let elt = new_link(x, y, "(view run)", &href); + group.add_child(elt); // Source URL + let href = format!("{}#l{}", call_ctx.source_filename, call_ctx.line); + let elt = new_link(x + 90.px(), y, "(view source)", &href); + group.add_child(elt); + + // Timings explanation y += delta_y; - let run = format!("{}#l{}", call_ctx.source_filename, call_ctx.line); - let mut elt = svg::new_text(x.0, y.0, "(view source)"); - elt.add_attr(Fill("royalblue".to_string())); - elt.add_attr(TextDecoration("underline".to_string())); - let mut a = new_a(&run); - a.add_child(elt); - group.add_child(a); + let elt = new_link( + x, + y, + "Explanation", + "https://hurl.dev/docs/response.html#timings", + ); + group.add_child(elt); group } +fn new_link(x: Pixel, y: Pixel, text: &str, href: &str) -> Element { + let mut elt = svg::new_text(x.0, y.0, text); + elt.add_attr(Fill("royalblue".to_string())); + elt.add_attr(TextDecoration("underline".to_string())); + let mut a = new_a(href); + a.add_child(elt); + a +} + /// Returns the highlighted span time of a call. fn new_call_sel( call: &Call, @@ -472,8 +487,10 @@ fn new_call_sel( let offset_x_start = to_pixel(offset_x_start, scale_x); let offset_x_end = (call.timings.end_call - times.start).to_std().unwrap(); let offset_x_end = to_pixel(offset_x_end, scale_x); - - let color = if call_ctx.success { "green" } else { "red" }; + let color = match call_ctx.kind { + CallContextKind::Success | CallContextKind::Retry => "green", + CallContextKind::Failure => "red", + }; let mut elt = svg::new_rect( offset_x_start.0, 0.0,