OXYGENHUB
# c8df33a6829f oxygenhub / crates / hub / src / api / reports.rs
5.4 KiB · Rust · last changed API: coverage and test reports 1 month ago source blame history raw
1
//! Coverage (oxygenhub#113) and test reports (#114) over the API: what the
2
//! Coverage and Tests tabs show, for whoever may read the repository.
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
    /// The bookmark (default: the repository's default).
16
    bookmark: Option<String>,
17
    /// At most this many, newest first (default 20, at most 200).
18
    limit: Option<i64>,
19
}
20
21
#[derive(Deserialize, Default)]
22
pub struct CasesQuery {
23
    /// `failed` for the failed and errored cases only.
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
/// `GET /repos/{o}/{n}/coverage`.
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
/// `GET /repos/{o}/{n}/coverage/{id}`.
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
/// `GET /repos/{o}/{n}/tests`.
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
/// `GET /repos/{o}/{n}/tests/{id}`.
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
}