• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24368153097

13 Apr 2026 09:36PM UTC coverage: 75.096% (-0.3%) from 75.445%
24368153097

Pull #35

github

web-flow
Merge 97a02fd78 into 4ba0f08f1
Pull Request #35: feat: meta section & refactoring

2518 of 3592 new or added lines in 47 files covered. (70.1%)

155 existing lines in 9 files now uncovered.

16781 of 22346 relevant lines covered (75.1%)

2495.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.93
/src/report/allure.rs
1
//! Allure TestOps compatible reporter.
2
//!
3
//! Writes individual JSON files per test in the Allure results format.
4
//! Each file contains test metadata, status, timing, and gRPC call steps.
5

6
use crate::report::Reporter;
7
use crate::state::{TestResult, TestResults};
8
use anyhow::Result;
9
use serde::Serialize;
10
use std::fs;
11
use std::path::PathBuf;
12
use uuid::Uuid;
13

14
/// Allure reporter — writes one result file per test.
15
pub struct AllureReporter {
16
    output_dir: PathBuf,
17
}
18

19
impl AllureReporter {
20
    /// Create a new Allure reporter writing to `output_dir`.
21
    /// Creates the directory if it doesn't exist.
22
    pub fn new(output_dir: PathBuf) -> Self {
6✔
23
        if let Err(e) = fs::create_dir_all(&output_dir) {
6✔
24
            eprintln!("Failed to create allure report directory: {}", e);
×
25
        }
6✔
26
        Self { output_dir }
6✔
27
    }
6✔
28
}
29

30
#[derive(Serialize)]
31
#[serde(rename_all = "camelCase")]
32
struct AllureResult {
33
    uuid: String,
34
    history_id: String,
35
    full_name: String,
36
    name: String,
37
    status: String,
38
    status_details: Option<StatusDetails>,
39
    start: u128,
40
    stop: u128,
41
    stage: String,
42
    labels: Vec<Label>,
43
    #[serde(skip_serializing_if = "Option::is_none")]
44
    steps: Option<Vec<Step>>,
45
    #[serde(skip_serializing_if = "Option::is_none")]
46
    attachments: Option<Vec<Attachment>>,
47
}
48

49
#[derive(Serialize)]
50
struct StatusDetails {
51
    #[serde(skip_serializing_if = "Option::is_none")]
52
    message: Option<String>,
53
    #[serde(skip_serializing_if = "Option::is_none")]
54
    trace: Option<String>,
55
    #[serde(skip_serializing_if = "Option::is_none")]
56
    flaky: Option<bool>,
57
    #[serde(skip_serializing_if = "Option::is_none")]
58
    known: Option<bool>,
59
    #[serde(skip_serializing_if = "Option::is_none")]
60
    muted: Option<bool>,
61
}
62

63
#[derive(Serialize)]
64
struct Label {
65
    name: String,
66
    value: String,
67
}
68

69
#[derive(Serialize)]
70
#[serde(rename_all = "camelCase")]
71
struct Step {
72
    name: String,
73
    status: String,
74
    #[serde(skip_serializing_if = "Option::is_none")]
75
    start: Option<u128>,
76
    #[serde(skip_serializing_if = "Option::is_none")]
77
    stop: Option<u128>,
78
    #[serde(skip_serializing_if = "Option::is_none")]
79
    attachments: Option<Vec<Attachment>>,
80
}
81

82
#[derive(Serialize)]
83
#[serde(rename_all = "camelCase")]
84
struct Attachment {
85
    name: String,
86
    source: String,
87
    #[serde(rename = "type")]
88
    content_type: String,
89
}
90

91
fn extract_test_name(path: &str) -> String {
8✔
92
    std::path::Path::new(path)
8✔
93
        .file_name()
8✔
94
        .and_then(|n| n.to_str())
8✔
95
        .unwrap_or(path)
8✔
96
        .to_string()
8✔
97
}
8✔
98

99
fn extract_suite_name(path: &str) -> String {
8✔
100
    std::path::Path::new(path)
8✔
101
        .parent()
8✔
102
        .and_then(|p| p.file_name())
8✔
103
        .and_then(|n| n.to_str())
8✔
104
        .unwrap_or("gRPC Tests")
8✔
105
        .to_string()
8✔
106
}
8✔
107

108
impl Reporter for AllureReporter {
109
    fn on_test_start(&self, _test_name: &str) {}
1✔
110

111
    fn on_test_end(&self, test_name: &str, result: &TestResult) {
8✔
112
        let uuid = Uuid::new_v4().to_string();
8✔
113
        let namespace = Uuid::NAMESPACE_OID;
8✔
114
        let history_id = Uuid::new_v5(&namespace, test_name.as_bytes()).to_string();
8✔
115

116
        let status = match result.status {
8✔
117
            crate::state::TestStatus::Pass => "passed",
5✔
118
            crate::state::TestStatus::Fail => "failed",
2✔
119
            crate::state::TestStatus::Skip => "skipped",
1✔
120
        };
121

122
        let status_details = if result.error_message.is_some() {
8✔
123
            Some(StatusDetails {
3✔
124
                message: result.error_message.clone(),
3✔
125
                trace: None,
3✔
126
                flaky: None,
3✔
127
                known: None,
3✔
128
                muted: None,
3✔
129
            })
3✔
130
        } else {
131
            None
5✔
132
        };
133

134
        let now = crate::time::now_unix_millis();
8✔
135
        let duration = result.duration_ms as u128;
8✔
136
        let start = now.saturating_sub(duration);
8✔
137

138
        let test_name_short = extract_test_name(test_name);
8✔
139
        let suite_name = extract_suite_name(test_name);
8✔
140

141
        let grpc_step = if result.grpc_duration_ms.is_some() {
8✔
142
            Some(Step {
143
                name: "gRPC call".to_string(),
6✔
144
                status: if result.status == crate::state::TestStatus::Pass {
6✔
145
                    "passed"
4✔
146
                } else {
147
                    "failed"
2✔
148
                }
149
                .to_string(),
6✔
150
                start: Some(start),
6✔
151
                stop: Some(now),
6✔
152
                attachments: None,
6✔
153
            })
154
        } else {
155
            None
2✔
156
        };
157

158
        let steps = grpc_step.map(|s| vec![s]);
8✔
159

160
        let report = AllureResult {
8✔
161
            uuid: uuid.clone(),
8✔
162
            history_id,
8✔
163
            full_name: test_name.to_string(),
8✔
164
            name: test_name_short,
8✔
165
            status: status.to_string(),
8✔
166
            status_details,
8✔
167
            start,
8✔
168
            stop: now,
8✔
169
            stage: "finished".to_string(),
8✔
170
            labels: vec![
8✔
171
                Label {
8✔
172
                    name: "language".to_string(),
8✔
173
                    value: "rust".to_string(),
8✔
174
                },
8✔
175
                Label {
8✔
176
                    name: "framework".to_string(),
8✔
177
                    value: "grpctestify".to_string(),
8✔
178
                },
8✔
179
                Label {
8✔
180
                    name: "suite".to_string(),
8✔
181
                    value: suite_name,
8✔
182
                },
8✔
183
                Label {
8✔
184
                    name: "feature".to_string(),
8✔
185
                    value: "gRPC Test".to_string(),
8✔
186
                },
8✔
187
            ],
8✔
188
            steps,
8✔
189
            attachments: None,
8✔
190
        };
8✔
191

192
        let file_name = format!("{}-result.json", uuid);
8✔
193
        let file_path = self.output_dir.join(file_name);
8✔
194

195
        match fs::File::create(&file_path) {
8✔
196
            Ok(file) => {
8✔
197
                if let Err(e) = serde_json::to_writer(&file, &report) {
8✔
NEW
198
                    tracing::warn!("Failed to serialize Allure report: {e}");
×
199
                }
8✔
200
            }
NEW
201
            Err(e) => {
×
NEW
202
                tracing::warn!("Failed to create Allure report file {:?}: {e}", file_path);
×
203
            }
204
        }
205
    }
8✔
206

207
    fn on_suite_end(&self, _results: &TestResults) -> Result<()> {
×
208
        Ok(())
×
209
    }
×
210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc