use runnerd_fixtures::{Daemon, Facts, JobRow, TIMED_OUT, daemon, facts};
use super::{Dashboard, fake};
use crate::format::sentence;
use crate::views::outcome_kind_class;
use crate::views::overview::reason_class;
async fn dashboard() -> (Dashboard, String, Daemon, Facts) {
let daemon = daemon();
let facts = facts(&daemon);
let mut fixtures = fake::Fixtures::new(runnerd_fixtures::NOW);
fixtures.status = daemon.status.clone();
fixtures.jobs = daemon.jobs.clone();
fixtures.samples.clear();
fixtures.records = daemon.records.clone();
fixtures.caches = daemon.caches.clone();
fixtures.savings = Some(daemon.savings.clone());
let dashboard = Dashboard::serving(&[], fixtures).await;
let cookie = dashboard.sign_in().await;
(dashboard, cookie, daemon, facts)
}
fn text(html: &str) -> String {
let mut text = String::new();
let mut tag = false;
for c in html.chars() {
match c {
'<' => tag = true,
'>' => {
tag = false;
text.push(' ');
}
c if !tag => text.push(c),
_ => {}
}
}
let text = text
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("&", "&");
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn row_of(html: &str, job: u64) -> &str {
let from = html
.find(&format!("href=\"/jobs/{job}\""))
.unwrap_or_else(|| panic!("no row links to job {job}"));
let rest = &html[from..];
let to = ["</li>", "</tr>"]
.iter()
.filter_map(|end| rest.find(end))
.min()
.unwrap_or(rest.len());
&rest[..to]
}
fn assert_rows(html: &str, facts: &[JobRow], figures: bool) {
for fact in facts {
let row = row_of(html, fact.job);
let badge = format!(
"<span class=\"badge {}\">{}</span>",
outcome_kind_class(fact.kind),
fact.label
);
assert!(row.contains(&badge), "{badge}: {row}");
assert!(text(row).contains(&fact.name), "{}: {row}", fact.name);
if figures {
for figure in [&fact.waited, &fact.ran].into_iter().flatten() {
assert!(row.contains(&format!(">{figure}<")), "{figure}: {row}");
}
}
}
}
#[tokio::test]
async fn the_overview_states_the_budget_health_slots_waits_and_latest_jobs_as_core_does() {
let (dashboard, cookie, _, facts) = dashboard().await;
let html = dashboard.page(&cookie, "/").await.body;
let shown = text(&html);
assert!(shown.contains(&facts.budget), "{shown}");
assert!(shown.contains(&facts.cpu), "{shown}");
for fact in [&facts.host, &facts.free, &facts.host_part] {
assert!(shown.contains(fact.as_str()), "{fact}: {shown}");
}
assert!(shown.contains(&facts.savings), "{}: {shown}", facts.savings);
let health = &facts.health;
for figure in [
format!("Running {} of {} slots", health.running, health.slots),
format!("{} runners starting", health.starting),
format!("{} warm", health.warm),
format!("Waiting {}", health.waiting),
] {
assert!(shown.contains(&figure), "{figure}: {shown}");
}
for slot in facts.slots.iter().filter(|slot| slot.word != "running") {
let line = sentence(&slot.line);
assert!(shown.contains(&line), "{line}: {shown}");
}
for wait in &facts.waits {
let reason = format!(
"<span class=\"reason {}\">{}</span>",
reason_class(wait.kind),
wait.reason
);
assert!(html.contains(&reason), "{reason}");
}
let latest = &facts.rows[..facts.rows.len() - 2];
assert_rows(&html, latest, false);
}
#[tokio::test]
async fn the_history_states_each_outcome_wait_and_run_time_as_core_does() {
let (dashboard, cookie, _, facts) = dashboard().await;
let html = dashboard.page(&cookie, "/history").await.body;
let jobs = &html[html.find("<table").unwrap()..];
assert_rows(jobs, &facts.rows, true);
}
#[tokio::test]
async fn a_job_states_its_outcome_steps_build_cache_and_failed_runs_as_core_does() {
let (dashboard, cookie, _, facts) = dashboard().await;
let html = dashboard
.page(&cookie, &format!("/jobs/{TIMED_OUT}"))
.await
.body;
let shown = text(&html);
let job = &facts.job;
let badge = format!(
"<span class=\"badge {}\">{}</span>",
outcome_kind_class(job.row.kind),
job.row.label
);
assert!(html.contains(&badge), "{badge}");
for figure in [
job.cpu_time.clone(),
format!("Failed {} of {}", job.failed, job.finished),
]
.iter()
.chain(&job.build_cache)
{
assert!(shown.contains(figure.as_str()), "{figure}: {shown}");
}
for step in &job.steps {
let name = format!("<span class=\"step-name\">{}</span>", step.name);
let from = html
.find(&name)
.unwrap_or_else(|| panic!("no step {}", step.name));
let item = &html[html[..from].rfind("<li").unwrap()..];
let item = &item[..item.find("</li>").unwrap()];
let class = format!("<li class=\"{}\">", outcome_kind_class(step.kind));
assert!(
item.starts_with(&class),
"{} is {:?}: {item}",
step.name,
step.kind
);
let took = step.took.as_deref().unwrap_or("–");
assert!(text(item).ends_with(took), "{took}: {item}");
}
}
#[tokio::test]
async fn records_and_the_build_cache_state_their_limits_as_core_does() {
let (dashboard, cookie, _, facts) = dashboard().await;
let html = dashboard.page(&cookie, "/caches").await.body;
let shown = text(&html);
assert!(shown.contains(&facts.build_cache_week), "{shown}");
let summary = format!("{}; {}", facts.records_usage, facts.records_pressure);
let at = html
.find(&sentence(&summary))
.unwrap_or_else(|| panic!("no summary {summary}: {shown}"));
let element = &html[html[..at].rfind('<').unwrap()..at];
assert_eq!(
element.contains("over-limit"),
facts.records_tone == runnerd_core::api::Tone::Warn,
"the summary stands out exactly when core's tone warns: {element}"
);
}