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,