| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
use super::{caller, not_found, reach, ApiError}; |
| 5 |
use crate::auth::Access; |
| 6 |
use crate::{coverage, junit, Shared}; |
| 7 |
use axum::extract::{Path, Query, State}; |
| 8 |
use axum::http::HeaderMap; |
| 9 |
use axum::Json; |
| 10 |
use hub_api::{CoverageDetail, CoverageFile, CoverageReport, TestCase, TestReport, TestReportDetail}; |
| 11 |
use serde::Deserialize; |
| 12 |
|
| 13 |
#[derive(Deserialize, Default)] |
| 14 |
pub struct ListQuery { |
| 15 |
|
| 16 |
bookmark: Option<String>, |
| 17 |
|
| 18 |
limit: Option<i64>, |
| 19 |
} |
| 20 |
|
| 21 |
#[derive(Deserialize, Default)] |
| 22 |
pub struct CasesQuery { |
| 23 |
|
| 24 |
status: Option<String>, |
| 25 |
} |
| 26 |
|
| 27 |
fn coverage_of(app: &Shared, base: &str, r: &coverage::Report) -> CoverageReport { |
| 28 |
CoverageReport { |
| 29 |
id: r.id, |
| 30 |
run: r.run_number, |
| 31 |
job: r.job.clone(), |
| 32 |
bookmark: r.bookmark.clone(), |
| 33 |
state: r.state.clone(), |
| 34 |
format: r.format.clone(), |
| 35 |
lines_found: r.lines_found, |
| 36 |
lines_hit: r.lines_hit, |
| 37 |
percent: r.percent(), |
| 38 |
functions_found: r.functions_found, |
| 39 |
functions_hit: r.functions_hit, |
| 40 |
branches_found: r.branches_found, |
| 41 |
branches_hit: r.branches_hit, |
| 42 |
files: r.files, |
| 43 |
dropped: r.dropped, |
| 44 |
created_ms: r.created_ms, |
| 45 |
url: format!("{}{base}/coverage?report={}", app.config.base_url().trim_end_matches('/'), r.id), |
| 46 |
} |
| 47 |
} |
| 48 |
|
| 49 |
fn tests_of(app: &Shared, base: &str, r: &junit::Report) -> TestReport { |
| 50 |
TestReport { |
| 51 |
id: r.id, |
| 52 |
run: r.run_number, |
| 53 |
job: r.job.clone(), |
| 54 |
bookmark: r.bookmark.clone(), |
| 55 |
state: r.state.clone(), |
| 56 |
tests: r.tests, |
| 57 |
passed: r.passed, |
| 58 |
failed: r.failed, |
| 59 |
errors: r.errors, |
| 60 |
skipped: r.skipped, |
| 61 |
time_ms: r.time_ms, |
| 62 |
created_ms: r.created_ms, |
| 63 |
url: format!("{}{base}/tests?report={}", app.config.base_url().trim_end_matches('/'), r.id), |
| 64 |
} |
| 65 |
} |
| 66 |
|
| 67 |
|
| 68 |
pub async fn coverage_list( |
| 69 |
State(app): State<Shared>, |
| 70 |
headers: HeaderMap, |
| 71 |
Path((owner, name)): Path<(String, String)>, |
| 72 |
Query(q): Query<ListQuery>, |
| 73 |
) -> Result<Json<Vec<CoverageReport>>, ApiError> { |
| 74 |
let c = caller(&app, &headers).await?; |
| 75 |
let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; |
| 76 |
let bookmark = q.bookmark.unwrap_or_else(|| row.default_bookmark.clone()); |
| 77 |
let list = coverage::on_bookmark(&app.db, row.id, &bookmark, q.limit.unwrap_or(20).clamp(1, 200)).await?; |
| 78 |
let base = format!("/{owner}/{name}"); |
| 79 |
Ok(Json(list.iter().map(|r| coverage_of(&app, &base, r)).collect())) |
| 80 |
} |
| 81 |
|
| 82 |
|
| 83 |
pub async fn coverage_one( |
| 84 |
State(app): State<Shared>, |
| 85 |
headers: HeaderMap, |
| 86 |
Path((owner, name, id)): Path<(String, String, i64)>, |
| 87 |
) -> Result<Json<CoverageDetail>, ApiError> { |
| 88 |
let c = caller(&app, &headers).await?; |
| 89 |
let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; |
| 90 |
let r = coverage::report(&app.db, row.id, id).await?.ok_or_else(|| not_found("no such coverage report"))?; |
| 91 |
let files = coverage::files(&app.db, r.id) |
| 92 |
.await? |
| 93 |
.into_iter() |
| 94 |
.map(|f| CoverageFile { |
| 95 |
percent: f.percent(), |
| 96 |
path: f.path, |
| 97 |
lines_found: f.lines_found, |
| 98 |
lines_hit: f.lines_hit, |
| 99 |
functions_found: f.functions_found, |
| 100 |
functions_hit: f.functions_hit, |
| 101 |
branches_found: f.branches_found, |
| 102 |
branches_hit: f.branches_hit, |
| 103 |
}) |
| 104 |
.collect(); |
| 105 |
Ok(Json(CoverageDetail { report: coverage_of(&app, &format!("/{owner}/{name}"), &r), files })) |
| 106 |
} |
| 107 |
|
| 108 |
|
| 109 |
pub async fn tests_list( |
| 110 |
State(app): State<Shared>, |
| 111 |
headers: HeaderMap, |
| 112 |
Path((owner, name)): Path<(String, String)>, |
| 113 |
Query(q): Query<ListQuery>, |
| 114 |
) -> Result<Json<Vec<TestReport>>, ApiError> { |
| 115 |
let c = caller(&app, &headers).await?; |
| 116 |
let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; |
| 117 |
let bookmark = q.bookmark.unwrap_or_else(|| row.default_bookmark.clone()); |
| 118 |
let list = junit::on_bookmark(&app.db, row.id, &bookmark, q.limit.unwrap_or(20).clamp(1, 200)).await?; |
| 119 |
let base = format!("/{owner}/{name}"); |
| 120 |
Ok(Json(list.iter().map(|r| tests_of(&app, &base, r)).collect())) |
| 121 |
} |
| 122 |
|
| 123 |
|
| 124 |
pub async fn tests_one( |
| 125 |
State(app): State<Shared>, |
| 126 |
headers: HeaderMap, |
| 127 |
Path((owner, name, id)): Path<(String, String, i64)>, |
| 128 |
Query(q): Query<CasesQuery>, |
| 129 |
) -> Result<Json<TestReportDetail>, ApiError> { |
| 130 |
let c = caller(&app, &headers).await?; |
| 131 |
let (_, row, _) = reach(&app, &c, &owner, &name, Access::Read).await?; |
| 132 |
let r = junit::report(&app.db, row.id, id).await?.ok_or_else(|| not_found("no such test report"))?; |
| 133 |
let statuses: &[&str] = match q.status.as_deref() { |
| 134 |
Some("failed") => &["failed", "error"], |
| 135 |
_ => &[], |
| 136 |
}; |
| 137 |
let cases = junit::cases(&app.db, r.id, statuses, junit::MAX_CASES as i64) |
| 138 |
.await? |
| 139 |
.into_iter() |
| 140 |
.map(|c| TestCase { |
| 141 |
suite: c.suite, |
| 142 |
classname: c.classname, |
| 143 |
name: c.name, |
| 144 |
status: c.status, |
| 145 |
time_ms: c.time_ms, |
| 146 |
message: c.message, |
| 147 |
output: c.output, |
| 148 |
}) |
| 149 |
.collect(); |
| 150 |
Ok(Json(TestReportDetail { report: tests_of(&app, &format!("/{owner}/{name}"), &r), cases })) |
| 151 |
} |