//! Coverage (oxygenhub#113) and test reports (#114) over the API: what the //! Coverage and Tests tabs show, for whoever may read the repository. use super::{caller, not_found, reach, ApiError}; use crate::auth::Access; use crate::{coverage, junit, Shared}; use axum::extract::{Path, Query, State}; use axum::http::HeaderMap; use axum::Json; use hub_api::{CoverageDetail, CoverageFile, CoverageReport, TestCase, TestReport, TestReportDetail}; use serde::Deserialize; #[derive(Deserialize, Default)] pub struct ListQuery { /// The bookmark (default: the repository's default). bookmark: Option, /// At most this many, newest first (default 20, at most 200). limit: Option, } #[derive(Deserialize, Default)] pub struct CasesQuery { /// `failed` for the failed and errored cases only. status: Option, } fn coverage_of(app: &Shared, base: &str, r: &coverage::Report) -> CoverageReport { CoverageReport { id: r.id, run: r.run_number, job: r.job.clone(), bookmark: r.bookmark.clone(), state: r.state.clone(), format: r.format.clone(), lines_found: r.lines_found, lines_hit: r.lines_hit, percent: r.percent(), functions_found: r.functions_found, functions_hit: r.functions_hit, branches_found: r.branches_found, branches_hit: r.branches_hit, files: r.files, dropped: r.dropped, created_ms: r.created_ms, url: format!("{}{base}/coverage?report={}", app.config.base_url().trim_end_matches('/'), r.id), } } fn tests_of(app: &Shared, base: &str, r: &junit::Report) -> TestReport { TestReport { id: r.id, run: r.run_number, job: r.job.clone(), bookmark: r.bookmark.clone(), state: r.state.clone(), tests: r.tests, passed: r.passed, failed: r.failed, errors: r.errors, skipped: r.skipped, time_ms: r.time_ms, created_ms: r.created_ms, url: format!("{}{base}/tests?report={}", app.config.base_url().trim_end_matches('/'), r.id), } } /// `GET /repos/{o}/{n}/coverage`. pub async fn coverage_list( State(app): State, headers: HeaderMap, Path((owner, name)): Path<(String, String)>, Query(q): Query, ) -> Result>, ApiError> { let c = caller(&app, &headers).await?; let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; let bookmark = q.bookmark.unwrap_or_else(|| row.default_bookmark.clone()); let list = coverage::on_bookmark(&app.db, row.id, &bookmark, q.limit.unwrap_or(20).clamp(1, 200)).await?; let base = format!("/{owner}/{name}"); Ok(Json(list.iter().map(|r| coverage_of(&app, &base, r)).collect())) } /// `GET /repos/{o}/{n}/coverage/{id}`. pub async fn coverage_one( State(app): State, headers: HeaderMap, Path((owner, name, id)): Path<(String, String, i64)>, ) -> Result, ApiError> { let c = caller(&app, &headers).await?; let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; let r = coverage::report(&app.db, row.id, id).await?.ok_or_else(|| not_found("no such coverage report"))?; let files = coverage::files(&app.db, r.id) .await? .into_iter() .map(|f| CoverageFile { percent: f.percent(), path: f.path, lines_found: f.lines_found, lines_hit: f.lines_hit, functions_found: f.functions_found, functions_hit: f.functions_hit, branches_found: f.branches_found, branches_hit: f.branches_hit, }) .collect(); Ok(Json(CoverageDetail { report: coverage_of(&app, &format!("/{owner}/{name}"), &r), files })) } /// `GET /repos/{o}/{n}/tests`. pub async fn tests_list( State(app): State, headers: HeaderMap, Path((owner, name)): Path<(String, String)>, Query(q): Query, ) -> Result>, ApiError> { let c = caller(&app, &headers).await?; let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; let bookmark = q.bookmark.unwrap_or_else(|| row.default_bookmark.clone()); let list = junit::on_bookmark(&app.db, row.id, &bookmark, q.limit.unwrap_or(20).clamp(1, 200)).await?; let base = format!("/{owner}/{name}"); Ok(Json(list.iter().map(|r| tests_of(&app, &base, r)).collect())) } /// `GET /repos/{o}/{n}/tests/{id}`. pub async fn tests_one( State(app): State, headers: HeaderMap, Path((owner, name, id)): Path<(String, String, i64)>, Query(q): Query, ) -> Result, ApiError> { let c = caller(&app, &headers).await?; let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; let r = junit::report(&app.db, row.id, id).await?.ok_or_else(|| not_found("no such test report"))?; let statuses: &[&str] = match q.status.as_deref() { Some("failed") => &["failed", "error"], _ => &[], }; let cases = junit::cases(&app.db, r.id, statuses, junit::MAX_CASES as i64) .await? .into_iter() .map(|c| TestCase { suite: c.suite, classname: c.classname, name: c.name, status: c.status, time_ms: c.time_ms, message: c.message, output: c.output, }) .collect(); Ok(Json(TestReportDetail { report: tests_of(&app, &format!("/{owner}/{name}"), &r), cases })) }