1pub mod enums;
34pub mod error;
35pub mod model;
36pub mod runtime;
37pub mod writer;
38
39pub use enums::{ContentType, LabelName, LinkType, ParameterMode, Severity, Stage, Status};
41pub use error::{AllureError, AllureResult};
42pub use model::{
43 Attachment, Category, FixtureResult, Label, Link, Parameter, StatusDetails, StepResult,
44 TestResult, TestResultContainer,
45};
46pub use runtime::{
47 allure_id, attach_binary, attach_file, attach_json, attach_text, configure, description,
48 description_html, display_name, epic, feature, flaky, issue, known_issue, label, link,
49 log_step, muted, owner, parameter, parameter_excluded, parameter_hidden, parameter_masked,
50 parent_suite, run_test, severity, skip, step, story, sub_suite, suite, tag, tags, test_case_id,
51 title, tms, with_async_context, with_context, with_test_context, AllureConfig,
52 AllureConfigBuilder, TestContext,
53};
54pub use writer::{compute_history_id, generate_uuid, AllureWriter, DEFAULT_RESULTS_DIR};
55
56#[cfg(feature = "async")]
58pub use futures;
59
60pub mod bdd {
62 use crate::runtime::step;
63
64 pub fn given<F, R>(description: impl Into<String>, body: F) -> R
66 where
67 F: FnOnce() -> R,
68 {
69 step(format!("Given {}", description.into()), body)
70 }
71
72 pub fn when<F, R>(description: impl Into<String>, body: F) -> R
74 where
75 F: FnOnce() -> R,
76 {
77 step(format!("When {}", description.into()), body)
78 }
79
80 pub fn then<F, R>(description: impl Into<String>, body: F) -> R
82 where
83 F: FnOnce() -> R,
84 {
85 step(format!("Then {}", description.into()), body)
86 }
87
88 pub fn and<F, R>(description: impl Into<String>, body: F) -> R
90 where
91 F: FnOnce() -> R,
92 {
93 step(format!("And {}", description.into()), body)
94 }
95
96 pub fn but<F, R>(description: impl Into<String>, body: F) -> R
98 where
99 F: FnOnce() -> R,
100 {
101 step(format!("But {}", description.into()), body)
102 }
103}
104
105pub mod attachment {
107 use crate::enums::ContentType;
108 use crate::runtime::{attach_binary, attach_file as attach_file_fn, attach_json, attach_text};
109
110 pub fn text(name: impl Into<String>, content: impl AsRef<str>) {
112 attach_text(name, content);
113 }
114
115 pub fn json<T: serde::Serialize>(name: impl Into<String>, value: &T) {
117 attach_json(name, value);
118 }
119
120 pub fn binary(name: impl Into<String>, content: &[u8], content_type: ContentType) {
122 attach_binary(name, content, content_type);
123 }
124
125 pub fn file(
127 name: impl Into<String>,
128 path: impl AsRef<std::path::Path>,
129 content_type: Option<ContentType>,
130 ) {
131 attach_file_fn(name, path, content_type);
132 }
133
134 pub fn png(name: impl Into<String>, content: &[u8]) {
136 attach_binary(name, content, ContentType::Png);
137 }
138
139 pub fn jpeg(name: impl Into<String>, content: &[u8]) {
141 attach_binary(name, content, ContentType::Jpeg);
142 }
143
144 pub fn html(name: impl Into<String>, content: impl AsRef<str>) {
146 attach_binary(name, content.as_ref().as_bytes(), ContentType::Html);
147 }
148
149 pub fn xml(name: impl Into<String>, content: impl AsRef<str>) {
151 attach_binary(name, content.as_ref().as_bytes(), ContentType::Xml);
152 }
153
154 pub fn csv(name: impl Into<String>, content: impl AsRef<str>) {
156 attach_binary(name, content.as_ref().as_bytes(), ContentType::Csv);
157 }
158
159 pub fn image_diff(name: impl Into<String>, content: &[u8]) {
161 attach_binary(name, content, ContentType::ImageDiff);
162 }
163}
164
165pub struct EnvironmentBuilder {
167 properties: Vec<(String, String)>,
168 results_dir: String,
169}
170
171impl EnvironmentBuilder {
172 pub fn new() -> Self {
174 Self {
175 properties: Vec::new(),
176 results_dir: DEFAULT_RESULTS_DIR.to_string(),
177 }
178 }
179
180 pub fn results_dir(mut self, path: impl Into<String>) -> Self {
182 self.results_dir = path.into();
183 self
184 }
185
186 pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
188 self.properties.push((key.into(), value.into()));
189 self
190 }
191
192 pub fn set_from_env(mut self, key: impl Into<String>, env_var: &str) -> Self {
194 if let Ok(value) = std::env::var(env_var) {
195 self.properties.push((key.into(), value));
196 }
197 self
198 }
199
200 pub fn write(self) -> std::io::Result<std::path::PathBuf> {
202 let writer = AllureWriter::with_results_dir(&self.results_dir);
203 writer.write_environment(&self.properties)
204 }
205}
206
207impl Default for EnvironmentBuilder {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213pub fn environment() -> EnvironmentBuilder {
215 EnvironmentBuilder::new()
216}
217
218pub struct CategoriesBuilder {
220 categories: Vec<Category>,
221 results_dir: String,
222}
223
224impl CategoriesBuilder {
225 pub fn new() -> Self {
227 Self {
228 categories: Vec::new(),
229 results_dir: DEFAULT_RESULTS_DIR.to_string(),
230 }
231 }
232
233 pub fn results_dir(mut self, path: impl Into<String>) -> Self {
235 self.results_dir = path.into();
236 self
237 }
238
239 pub fn with_category(mut self, category: Category) -> Self {
241 self.categories.push(category);
242 self
243 }
244
245 pub fn with_product_defects(mut self) -> Self {
247 self.categories
248 .push(Category::new("Product defects").with_status(Status::Failed));
249 self
250 }
251
252 pub fn with_test_defects(mut self) -> Self {
254 self.categories
255 .push(Category::new("Test defects").with_status(Status::Broken));
256 self
257 }
258
259 pub fn write(self) -> std::io::Result<std::path::PathBuf> {
261 let writer = AllureWriter::with_results_dir(&self.results_dir);
262 writer.write_categories(&self.categories)
263 }
264}
265
266impl Default for CategoriesBuilder {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272pub fn categories() -> CategoriesBuilder {
274 CategoriesBuilder::new()
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::runtime::{set_context, take_context, TestContext};
281 use crate::writer::AllureWriter;
282 use serde_json::json;
283 use std::fs;
284 use tempfile::tempdir;
285
286 #[test]
287 fn test_bdd_step_names() {
288 let result = bdd::given("a value", || 42);
290 assert_eq!(result, 42);
291
292 let result = bdd::when("something happens", || "ok");
293 assert_eq!(result, "ok");
294
295 let result = bdd::then("we check", || true);
296 assert!(result);
297 }
298
299 #[test]
300 fn test_environment_builder() {
301 let builder = environment().set("key1", "value1").set("key2", "value2");
302
303 assert_eq!(builder.properties.len(), 2);
304 }
305
306 #[test]
307 fn test_categories_builder() {
308 let builder = categories()
309 .with_product_defects()
310 .with_test_defects()
311 .with_category(Category::new("Custom").with_status(Status::Skipped));
312
313 assert_eq!(builder.categories.len(), 3);
314 }
315
316 #[test]
317 fn test_builder_defaults_use_default_dir() {
318 let env_builder = EnvironmentBuilder::default();
319 assert_eq!(env_builder.results_dir, DEFAULT_RESULTS_DIR);
320
321 let cat_builder = CategoriesBuilder::default();
322 assert_eq!(cat_builder.results_dir, DEFAULT_RESULTS_DIR);
323 }
324
325 #[test]
326 fn test_attachment_helpers_write_all_types() {
327 let temp = tempdir().unwrap();
328 let mut ctx = TestContext::new("attach", "module::attach");
329 ctx.writer = AllureWriter::with_results_dir(temp.path());
330 ctx.writer.init(true).unwrap();
331
332 let file_path = temp.path().join("sample.txt");
333 fs::write(&file_path, "sample file").unwrap();
334
335 set_context(ctx);
336
337 attachment::text("Text", "hello");
338 attachment::json("Json", &json!({"field": "value"}));
339 attachment::binary("Bin", b"\x00\x01", ContentType::Zip);
340 attachment::png("Png", b"png-bytes");
341 attachment::jpeg("Jpeg", b"jpeg-bytes");
342 attachment::html("Html", "<p>hi</p>");
343 attachment::xml("Xml", "<xml/>");
344 attachment::csv("Csv", "a,b,c");
345 attachment::image_diff("Diff", b"{}");
346 attachment::file("File", &file_path, None);
347
348 let ctx = take_context().unwrap();
349 assert_eq!(ctx.result.attachments.len(), 10);
350
351 let mut kinds = Vec::new();
352 for att in &ctx.result.attachments {
353 let path = temp.path().join(&att.source);
354 assert!(path.exists(), "attachment file missing: {}", att.source);
355 kinds.push(att.r#type.clone());
356 }
357
358 let has = |mime: &str| kinds.iter().any(|t| t.as_deref() == Some(mime));
359 assert!(has("text/plain"));
360 assert!(has("application/json"));
361 assert!(has("application/zip"));
362 assert!(has("image/png"));
363 assert!(has("image/jpeg"));
364 assert!(has("text/html"));
365 assert!(has("application/xml"));
366 assert!(has("text/csv"));
367 assert!(has("application/vnd.allure.image.diff"));
368 }
369
370 #[test]
371 fn test_environment_builder_set_from_env_and_write() {
372 let temp = tempdir().unwrap();
373 std::env::set_var("ALLURE_ENV_TEST_KEY", "from_env");
374
375 let path = environment()
376 .results_dir(temp.path().to_string_lossy().to_string())
377 .set("key", "value")
378 .set_from_env("env_key", "ALLURE_ENV_TEST_KEY")
379 .write()
380 .unwrap();
381
382 let contents = fs::read_to_string(path).unwrap();
383 assert!(contents.contains("key=value"));
384 assert!(contents.contains("env_key=from_env"));
385 }
386
387 #[test]
388 fn test_categories_builder_write_includes_defaults() {
389 let temp = tempdir().unwrap();
390 let path = categories()
391 .results_dir(temp.path().to_string_lossy().to_string())
392 .with_product_defects()
393 .with_test_defects()
394 .with_category(
395 Category::new("Custom")
396 .with_status(Status::Skipped)
397 .with_message_regex("oops")
398 .with_trace_regex("trace")
399 .as_flaky(),
400 )
401 .write()
402 .unwrap();
403
404 let contents = fs::read_to_string(path).unwrap();
405 let cats: Vec<Category> = serde_json::from_str(&contents).unwrap();
406 assert_eq!(cats.len(), 3);
407 assert!(cats.iter().any(|c| c.name == "Product defects"));
408 assert!(cats.iter().any(|c| c.name == "Test defects"));
409 let custom = cats.iter().find(|c| c.name == "Custom").unwrap();
410 assert_eq!(custom.matched_statuses, vec![Status::Skipped]);
411 assert_eq!(custom.message_regex.as_deref(), Some("oops"));
412 assert_eq!(custom.trace_regex.as_deref(), Some("trace"));
413 assert_eq!(custom.flaky, Some(true));
414 }
415}