1use std::{
4 any::Any,
5 cell::RefCell,
6 fmt, fs,
7 future::Future,
8 panic::{self, AssertUnwindSafe},
9 path::Path,
10 pin::Pin,
11 process::{ExitCode, Termination},
12 sync::{
13 atomic::{AtomicU64, Ordering},
14 OnceLock,
15 },
16 task::{Context, Poll},
17 time::{SystemTime, UNIX_EPOCH},
18};
19
20use crate::{
21 config::{
22 apply_common_runtime_labels, apply_config_labels, apply_synthetic_suite_labels,
23 title_path as config_title_path,
24 },
25 current_owner, error_classifier,
26 http_exchange::{HttpExchange, HTTP_EXCHANGE_ATTACHMENT_NAME},
27 lifecycle::{AllureLifecycle, AllureRuntime, StartTestCaseParams},
28 model::{GlobalAttachment, GlobalError, Globals, Label, ParameterMode, Status, StatusDetails},
29 writer::{FileSystemResultsWriter, PLAYWRIGHT_TRACE_ATTACHMENT_MIME},
30};
31
32static ALLURE: OnceLock<AllureFacade> = OnceLock::new();
33static FACADE_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
34
35thread_local! {
36 static CURRENT_ALLURE: RefCell<Option<AllureFacade>> = const { RefCell::new(None) };
37 static LAST_ASSERTION_FAILURE: RefCell<Option<StatusDetails>> = const { RefCell::new(None) };
38}
39
40pub fn allure() -> &'static AllureFacade {
42 ALLURE.get_or_init(AllureFacade::default)
43}
44
45pub struct CurrentAllureGuard {
47 previous: Option<AllureFacade>,
48}
49
50pub fn push_current_allure(allure: &AllureFacade) -> CurrentAllureGuard {
52 let previous = CURRENT_ALLURE.with(|current| current.replace(Some(allure.clone())));
53 CurrentAllureGuard { previous }
54}
55
56pub fn current_allure() -> Option<AllureFacade> {
58 CURRENT_ALLURE.with(|current| current.borrow().clone())
59}
60
61pub fn clear_last_assertion_failure() {
63 LAST_ASSERTION_FAILURE.with(|failure| {
64 failure.replace(None);
65 });
66}
67
68pub fn status_details_for_message(message: String) -> StatusDetails {
70 LAST_ASSERTION_FAILURE
71 .with(|failure| failure.take())
72 .filter(|details| details.message.as_deref() == Some(message.as_str()))
73 .unwrap_or(StatusDetails {
74 message: Some(message),
75 trace: None,
76 actual: None,
77 expected: None,
78 })
79}
80
81pub fn record_assertion_pass(name: impl Into<String>) {
83 if let Some(allure) = current_allure() {
84 allure.log_step(name);
85 }
86}
87
88#[track_caller]
89pub fn fail_assertion(
91 name: impl Into<String>,
92 message: String,
93 actual: Option<String>,
94 expected: Option<String>,
95) -> ! {
96 let details = StatusDetails {
97 message: Some(message.clone()),
98 trace: None,
99 actual,
100 expected,
101 };
102 LAST_ASSERTION_FAILURE.with(|failure| {
103 failure.replace(Some(details.clone()));
104 });
105 if let Some(allure) = current_allure() {
106 let guard = allure.start_step_scope(name);
107 let guard = guard.with_status(Status::Failed, Some(details));
108 drop(guard);
109 }
110 panic!("{message}");
111}
112
113fn active_allure() -> AllureFacade {
114 current_allure().unwrap_or_default()
115}
116
117fn facade_id() -> String {
118 let millis = SystemTime::now()
119 .duration_since(UNIX_EPOCH)
120 .map(|d| d.as_millis())
121 .unwrap_or_default();
122 let counter = FACADE_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
123 format!("{millis}-{counter}")
124}
125
126fn write_globals(globals: Globals) -> std::io::Result<()> {
127 FileSystemResultsWriter::from_env()?
128 .write_globals_typed(&globals)
129 .map(|_| ())
130}
131
132#[derive(Clone, Debug)]
134pub struct TestOptions {
135 params: StartTestCaseParams,
136 manifest_dir: Option<String>,
137 module_path: Option<String>,
138 title_path: Option<Vec<String>>,
139 framework: Option<String>,
140 synthetic_suite_labels: bool,
141 panic_status: Option<Status>,
142}
143
144impl TestOptions {
145 pub fn new(name: impl Into<String>) -> Self {
147 Self {
148 params: StartTestCaseParams::new(name),
149 manifest_dir: None,
150 module_path: None,
151 title_path: None,
152 framework: Some("cargo-test".to_string()),
153 synthetic_suite_labels: true,
154 panic_status: None,
155 }
156 }
157
158 pub fn inferred() -> Self {
160 let full_name = current_test_full_name();
161 let name = test_name_from_full_name(&full_name);
162 Self::new(name).with_full_name(full_name)
163 }
164
165 pub fn from_params(params: StartTestCaseParams) -> Self {
167 Self {
168 params,
169 manifest_dir: None,
170 module_path: None,
171 title_path: None,
172 framework: Some("cargo-test".to_string()),
173 synthetic_suite_labels: true,
174 panic_status: None,
175 }
176 }
177
178 pub fn with_full_name(mut self, full_name: impl Into<String>) -> Self {
180 self.params.full_name = Some(full_name.into());
181 self
182 }
183
184 pub fn with_history_id(mut self, history_id: impl Into<String>) -> Self {
186 self.params.history_id = Some(history_id.into());
187 self
188 }
189
190 pub fn with_test_case_id(mut self, test_case_id: impl Into<String>) -> Self {
192 self.params.test_case_id = Some(test_case_id.into());
193 self
194 }
195
196 pub fn with_description(mut self, description: impl Into<String>) -> Self {
198 self.params.description = Some(description.into());
199 self
200 }
201
202 pub fn with_description_html(mut self, description: impl Into<String>) -> Self {
204 self.params.description_html = Some(description.into());
205 self
206 }
207
208 pub fn with_label(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
210 self.params.labels.push(Label {
211 name: name.into(),
212 value: value.into(),
213 });
214 self
215 }
216
217 pub fn with_allure_id(self, value: impl Into<String>) -> Self {
219 self.with_label("ALLURE_ID", value)
220 }
221
222 pub fn with_manifest_dir(mut self, manifest_dir: impl Into<String>) -> Self {
224 self.manifest_dir = Some(manifest_dir.into());
225 self
226 }
227
228 pub fn with_module_path(mut self, module_path: impl Into<String>) -> Self {
230 self.module_path = Some(module_path.into());
231 self
232 }
233
234 pub fn with_title_path<I, V>(mut self, title_path: I) -> Self
236 where
237 I: IntoIterator<Item = V>,
238 V: Into<String>,
239 {
240 self.title_path = Some(title_path.into_iter().map(Into::into).collect());
241 self
242 }
243
244 pub fn with_source(
246 mut self,
247 file: impl AsRef<str>,
248 manifest_dir: impl Into<String>,
249 module_path: impl Into<String>,
250 ) -> Self {
251 let manifest_dir = manifest_dir.into();
252 self.title_path = Some(config_title_path(file.as_ref(), &manifest_dir));
253 self.manifest_dir = Some(manifest_dir);
254 self.module_path = Some(module_path.into());
255 self
256 }
257
258 pub fn with_framework(mut self, framework: impl Into<String>) -> Self {
260 self.framework = Some(framework.into());
261 self
262 }
263
264 pub fn without_framework(mut self) -> Self {
266 self.framework = None;
267 self
268 }
269
270 pub fn without_synthetic_suite_labels(mut self) -> Self {
272 self.synthetic_suite_labels = false;
273 self
274 }
275
276 pub fn with_panic_status(mut self, status: Status) -> Self {
278 self.panic_status = Some(status);
279 self
280 }
281}
282
283impl From<StartTestCaseParams> for TestOptions {
284 fn from(params: StartTestCaseParams) -> Self {
285 Self::from_params(params)
286 }
287}
288
289pub trait AllureTestOutcome: Sized {
294 fn successful() -> Self;
296
297 fn status(&self) -> (Status, Option<StatusDetails>);
299}
300
301impl AllureTestOutcome for () {
302 fn successful() -> Self {}
303
304 fn status(&self) -> (Status, Option<StatusDetails>) {
305 (Status::Passed, None)
306 }
307}
308
309impl<T, E> AllureTestOutcome for Result<T, E>
310where
311 T: AllureTestOutcome,
312 E: fmt::Debug,
313{
314 fn successful() -> Self {
315 Ok(T::successful())
316 }
317
318 fn status(&self) -> (Status, Option<StatusDetails>) {
319 match self {
320 Ok(value) => value.status(),
321 Err(error) => {
322 let (status, details) = error_classifier::classify_message(format!("{error:?}"));
323 (status, Some(details))
324 }
325 }
326 }
327}
328
329impl AllureTestOutcome for ExitCode {
330 fn successful() -> Self {
331 ExitCode::SUCCESS
332 }
333
334 fn status(&self) -> (Status, Option<StatusDetails>) {
335 if self == &ExitCode::SUCCESS {
336 (Status::Passed, None)
337 } else {
338 (
339 Status::Broken,
340 Some(status_details_for_message(
341 "test returned unsuccessful ExitCode".to_string(),
342 )),
343 )
344 }
345 }
346}
347
348#[doc(hidden)]
350pub struct AllureOutcomeReport {
351 pub exit_code: ExitCode,
353 pub status: Status,
355 pub details: Option<StatusDetails>,
357}
358
359#[doc(hidden)]
361pub struct AllureOutcomeProbe<R> {
362 value: Option<R>,
363}
364
365impl<R> AllureOutcomeProbe<R> {
366 pub fn new(value: R) -> Self {
368 Self { value: Some(value) }
369 }
370}
371
372#[doc(hidden)]
374pub trait FinishAllureOutcome {
375 fn finish_allure_outcome(self) -> std::thread::Result<AllureOutcomeReport>;
377}
378
379impl<R> FinishAllureOutcome for AllureOutcomeProbe<R>
380where
381 R: AllureTestOutcome + Termination,
382{
383 fn finish_allure_outcome(mut self) -> std::thread::Result<AllureOutcomeReport> {
384 let (status, details) = self
385 .value
386 .as_ref()
387 .expect("allure outcome value should be available")
388 .status();
389 let value = self
390 .value
391 .take()
392 .expect("allure outcome value should be available");
393 let exit_code = panic::catch_unwind(AssertUnwindSafe(|| value.report()))?;
394
395 Ok(AllureOutcomeReport {
396 exit_code,
397 status,
398 details,
399 })
400 }
401}
402
403impl<R> FinishAllureOutcome for &mut AllureOutcomeProbe<R>
404where
405 R: Termination,
406{
407 fn finish_allure_outcome(self) -> std::thread::Result<AllureOutcomeReport> {
408 let value = self
409 .value
410 .take()
411 .expect("allure outcome value should be available");
412 let exit_code = panic::catch_unwind(AssertUnwindSafe(|| value.report()))?;
413 let (status, details) = if exit_code == ExitCode::SUCCESS {
414 (Status::Passed, None)
415 } else {
416 (
417 Status::Broken,
418 Some(status_details_for_message(
419 "test returned unsuccessful Termination status".to_string(),
420 )),
421 )
422 };
423
424 Ok(AllureOutcomeReport {
425 exit_code,
426 status,
427 details,
428 })
429 }
430}
431
432pub fn test<R, F>(body: F) -> R
434where
435 F: FnOnce() -> R,
436{
437 test_with(TestOptions::inferred(), body)
438}
439
440pub fn test_named<R, F>(name: impl Into<String>, body: F) -> R
442where
443 F: FnOnce() -> R,
444{
445 let full_name = current_test_full_name();
446 test_with(TestOptions::new(name).with_full_name(full_name), body)
447}
448
449pub fn test_with<R, F>(options: impl Into<TestOptions>, body: F) -> R
451where
452 F: FnOnce() -> R,
453{
454 let runtime_test = start_runtime_test(options.into());
455 let _current_allure = push_current_allure(&runtime_test.allure);
456 let result = panic::catch_unwind(AssertUnwindSafe(body));
457 finish_runtime_test(runtime_test, result)
458}
459
460pub fn test_with_outcome<R, F>(options: impl Into<TestOptions>, body: F) -> R
462where
463 R: AllureTestOutcome,
464 F: FnOnce() -> R,
465{
466 let runtime_test = start_runtime_test(options.into());
467 let _current_allure = push_current_allure(&runtime_test.allure);
468 let result = panic::catch_unwind(AssertUnwindSafe(body));
469 finish_runtime_test_with_outcome(runtime_test, result)
470}
471
472pub async fn test_async<R, F>(future: F) -> R
474where
475 F: Future<Output = R>,
476{
477 test_with_async(TestOptions::inferred(), future).await
478}
479
480pub async fn test_named_async<R, F>(name: impl Into<String>, future: F) -> R
482where
483 F: Future<Output = R>,
484{
485 let full_name = current_test_full_name();
486 test_with_async(TestOptions::new(name).with_full_name(full_name), future).await
487}
488
489pub async fn test_with_async<R, F>(options: impl Into<TestOptions>, future: F) -> R
491where
492 F: Future<Output = R>,
493{
494 let runtime_test = start_runtime_test(options.into());
495 let result = RuntimeTestFuture {
496 allure: runtime_test.allure.clone(),
497 future: Box::pin(future),
498 }
499 .await;
500 finish_runtime_test(runtime_test, result)
501}
502
503pub async fn test_with_outcome_async<R, F>(options: impl Into<TestOptions>, future: F) -> R
505where
506 R: AllureTestOutcome,
507 F: Future<Output = R>,
508{
509 let runtime_test = start_runtime_test(options.into());
510 let result = RuntimeTestFuture {
511 allure: runtime_test.allure.clone(),
512 future: Box::pin(future),
513 }
514 .await;
515 finish_runtime_test_with_outcome(runtime_test, result)
516}
517
518#[doc(hidden)]
520pub struct RuntimeTest {
521 allure: AllureFacade,
522 panic_status: Option<Status>,
523}
524
525fn start_runtime_test(options: TestOptions) -> RuntimeTest {
526 let writer = FileSystemResultsWriter::from_env().expect("allure writer should be created");
527 let runtime = AllureRuntime::new(writer);
528 let allure = AllureFacade::with_lifecycle(runtime.lifecycle());
529 let TestOptions {
530 params,
531 manifest_dir,
532 module_path,
533 title_path,
534 framework,
535 synthetic_suite_labels,
536 panic_status,
537 } = options;
538 let full_name = params.full_name.clone();
539
540 allure.start_test_case(params);
541 clear_last_assertion_failure();
542 apply_common_runtime_labels(&allure);
543 if let Some(framework) = framework {
544 allure.label("framework", framework);
545 }
546 if synthetic_suite_labels {
547 apply_synthetic_suite_labels(&allure, full_name.as_deref());
548 }
549 if let Some(title_path) = title_path {
550 if let (Some(manifest_dir), Some(module_path)) = (&manifest_dir, &module_path) {
551 apply_config_labels(&allure, manifest_dir, module_path, &title_path);
552 }
553 allure.title_path(title_path);
554 } else if let (Some(manifest_dir), Some(module_path)) = (&manifest_dir, &module_path) {
555 apply_config_labels(&allure, manifest_dir, module_path, &[]);
556 }
557
558 RuntimeTest {
559 allure,
560 panic_status,
561 }
562}
563
564#[doc(hidden)]
566pub fn start_runtime_test_with_options(options: impl Into<TestOptions>) -> RuntimeTest {
567 start_runtime_test(options.into())
568}
569
570#[doc(hidden)]
572pub fn push_runtime_test_allure(runtime_test: &RuntimeTest) -> CurrentAllureGuard {
573 push_current_allure(&runtime_test.allure)
574}
575
576#[doc(hidden)]
578pub async fn run_runtime_test_future<R, F>(
579 runtime_test: &RuntimeTest,
580 future: F,
581) -> std::thread::Result<R>
582where
583 F: Future<Output = R>,
584{
585 RuntimeTestFuture {
586 allure: runtime_test.allure.clone(),
587 future: Box::pin(future),
588 }
589 .await
590}
591
592#[doc(hidden)]
594pub fn finish_runtime_test_status(
595 runtime_test: RuntimeTest,
596 status: Status,
597 details: Option<StatusDetails>,
598) {
599 runtime_test.allure.end_test(status, details);
600}
601
602#[doc(hidden)]
604pub fn finish_runtime_test_panic(runtime_test: RuntimeTest, payload: Box<dyn Any + Send>) -> ! {
605 let (status, details) = error_classifier::classify_panic(&payload);
606 let status = runtime_test.panic_status.unwrap_or(status);
607 let message = details.message.clone().unwrap_or_default();
608 runtime_test
609 .allure
610 .end_test(status, Some(status_details_for_message(message)));
611 panic::resume_unwind(payload);
612}
613
614fn finish_runtime_test<R>(runtime_test: RuntimeTest, result: std::thread::Result<R>) -> R {
615 match result {
616 Ok(value) => {
617 runtime_test.allure.end_test(Status::Passed, None);
618 value
619 }
620 Err(payload) => finish_runtime_test_panic(runtime_test, payload),
621 }
622}
623
624fn finish_runtime_test_with_outcome<R>(
625 runtime_test: RuntimeTest,
626 result: std::thread::Result<R>,
627) -> R
628where
629 R: AllureTestOutcome,
630{
631 match result {
632 Ok(value) => {
633 let (status, details) = value.status();
634 runtime_test.allure.end_test(status, details);
635 value
636 }
637 Err(payload) => finish_runtime_test_panic(runtime_test, payload),
638 }
639}
640
641fn current_test_full_name() -> String {
642 std::thread::current()
643 .name()
644 .map(ToString::to_string)
645 .filter(|name| !name.trim().is_empty())
646 .unwrap_or_else(|| format!("{:?}", std::thread::current().id()))
647}
648
649fn test_name_from_full_name(full_name: &str) -> String {
650 full_name
651 .rsplit("::")
652 .next()
653 .filter(|name| !name.is_empty())
654 .unwrap_or(full_name)
655 .to_string()
656}
657
658struct RuntimeTestFuture<F> {
659 allure: AllureFacade,
660 future: Pin<Box<F>>,
661}
662
663impl<F> Future for RuntimeTestFuture<F>
664where
665 F: Future,
666{
667 type Output = std::thread::Result<F::Output>;
668
669 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
670 let this = self.get_mut();
671 let _current_allure = push_current_allure(&this.allure);
672 match panic::catch_unwind(AssertUnwindSafe(|| this.future.as_mut().poll(cx))) {
673 Ok(Poll::Ready(output)) => Poll::Ready(Ok(output)),
674 Ok(Poll::Pending) => Poll::Pending,
675 Err(payload) => Poll::Ready(Err(payload)),
676 }
677 }
678}
679
680pub fn description(description: impl Into<String>) {
682 active_allure().description(description);
683}
684
685pub fn description_html(description: impl Into<String>) {
687 active_allure().description_html(description);
688}
689
690pub fn display_name(name: impl Into<String>) {
692 active_allure().display_name(name);
693}
694
695pub fn label(name: impl Into<String>, value: impl Into<String>) {
697 active_allure().label(name, value);
698}
699
700pub fn labels<I, K, V>(labels: I)
702where
703 I: IntoIterator<Item = (K, V)>,
704 K: Into<String>,
705 V: Into<String>,
706{
707 active_allure().labels(labels);
708}
709
710pub fn link<U, N, T>(url: U, name: Option<N>, link_type: Option<T>)
712where
713 U: Into<String>,
714 N: Into<String>,
715 T: Into<String>,
716{
717 active_allure().link(url, name.map(Into::into), link_type.map(Into::into));
718}
719
720pub fn links<I, U, N, T>(links: I)
722where
723 I: IntoIterator<Item = (U, Option<N>, Option<T>)>,
724 U: Into<String>,
725 N: Into<String>,
726 T: Into<String>,
727{
728 active_allure().links(links);
729}
730
731pub fn parameter(name: impl Into<String>, value: impl Into<String>) {
733 active_allure().parameter(name, value);
734}
735
736pub fn parameter_excluded(name: impl Into<String>, value: impl Into<String>, excluded: bool) {
738 active_allure().parameter_excluded(name, value, excluded);
739}
740
741pub fn parameter_mode(name: impl Into<String>, value: impl Into<String>, mode: ParameterMode) {
743 active_allure().parameter_mode(name, value, mode);
744}
745
746pub fn parameter_with_options(
748 name: impl Into<String>,
749 value: impl Into<String>,
750 excluded: Option<bool>,
751 mode: Option<ParameterMode>,
752) {
753 active_allure().parameter_with_options(name, value, excluded, mode);
754}
755
756pub fn set_title_path<I, V>(title_path: I)
758where
759 I: IntoIterator<Item = V>,
760 V: Into<String>,
761{
762 active_allure().title_path(title_path);
763}
764
765pub fn test_case_id(value: impl Into<String>) {
767 active_allure().test_case_id(value);
768}
769
770pub fn history_id(value: impl Into<String>) {
772 active_allure().history_id(value);
773}
774
775pub fn attachment(
777 name: impl Into<String>,
778 content_type: impl Into<String>,
779 body: impl AsRef<[u8]>,
780) {
781 active_allure().attachment(name, content_type, body);
782}
783
784pub fn attachment_path(
786 name: impl Into<String>,
787 content_type: impl Into<String>,
788 path: impl AsRef<Path>,
789) -> std::io::Result<()> {
790 active_allure().attachment_path(name, content_type, path)
791}
792
793pub fn enter_step(name: impl Into<String>) -> StepGuard {
795 active_allure().enter_step(name)
796}
797
798pub fn attach_trace(path: impl AsRef<Path>) -> std::io::Result<()> {
800 active_allure().attach_trace(path)
801}
802
803pub fn attach_trace_named(name: impl Into<String>, path: impl AsRef<Path>) -> std::io::Result<()> {
805 active_allure().attach_trace_named(name, path)
806}
807
808pub fn global_attachment(
810 name: impl Into<String>,
811 content_type: impl Into<String>,
812 body: impl AsRef<[u8]>,
813) -> std::io::Result<()> {
814 active_allure().global_attachment(name, content_type, body)
815}
816
817pub fn global_attachment_path(
819 name: impl Into<String>,
820 content_type: impl Into<String>,
821 path: impl AsRef<Path>,
822) -> std::io::Result<()> {
823 active_allure().global_attachment_path(name, content_type, path)
824}
825
826pub fn global_error(message: impl Into<String>) -> std::io::Result<()> {
828 active_allure().global_error(message)
829}
830
831pub fn global_error_with_trace(
833 message: impl Into<String>,
834 trace: impl Into<String>,
835) -> std::io::Result<()> {
836 active_allure().global_error_with_trace(message, trace)
837}
838
839pub fn http_exchange(exchange: HttpExchange) {
841 active_allure().http_exchange(exchange);
842}
843
844pub fn http_exchange_named(name: impl Into<String>, exchange: HttpExchange) {
846 active_allure().http_exchange_named(name, exchange);
847}
848
849pub fn step<T, F>(name: impl Into<String>, body: F) -> T
851where
852 F: FnOnce() -> T,
853{
854 active_allure().step(name, body)
855}
856
857pub fn stage(name: impl Into<String>) {
859 active_allure().stage(name);
860}
861
862pub fn log_step(name: impl Into<String>) {
864 active_allure().log_step(name);
865}
866
867pub fn log_step_with<E>(name: impl Into<String>, status: Option<Status>, error: Option<E>)
869where
870 E: ToString,
871{
872 active_allure().log_step_with(name, status, error);
873}
874
875pub fn issue(name: impl Into<String>, url: impl Into<String>) {
877 active_allure().issue(name, url);
878}
879
880pub fn tms(name: impl Into<String>, url: impl Into<String>) {
882 active_allure().tms(name, url);
883}
884
885pub fn epic(value: impl Into<String>) {
887 active_allure().epic(value);
888}
889
890pub fn feature(value: impl Into<String>) {
892 active_allure().feature(value);
893}
894
895pub fn story(value: impl Into<String>) {
897 active_allure().story(value);
898}
899
900pub fn suite(value: impl Into<String>) {
902 active_allure().suite(value);
903}
904
905pub fn parent_suite(value: impl Into<String>) {
907 active_allure().parent_suite(value);
908}
909
910pub fn sub_suite(value: impl Into<String>) {
912 active_allure().sub_suite(value);
913}
914
915pub fn owner(value: impl Into<String>) {
917 active_allure().owner(value);
918}
919
920pub fn severity(value: impl Into<String>) {
922 active_allure().severity(value);
923}
924
925pub fn layer(value: impl Into<String>) {
927 active_allure().layer(value);
928}
929
930pub fn tag(value: impl Into<String>) {
932 active_allure().tag(value);
933}
934
935pub fn tags<I, V>(tags: I)
937where
938 I: IntoIterator<Item = V>,
939 V: Into<String>,
940{
941 active_allure().tags(tags);
942}
943
944pub fn id(value: impl Into<String>) {
946 active_allure().id(value);
947}
948
949pub fn allure_id(value: impl Into<String>) {
951 active_allure().allure_id(value);
952}
953
954impl Drop for CurrentAllureGuard {
955 fn drop(&mut self) {
956 CURRENT_ALLURE.with(|current| {
957 current.replace(self.previous.take());
958 });
959 }
960}
961
962#[derive(Clone, Default)]
964pub struct AllureFacade {
965 lifecycle: Option<AllureLifecycle>,
966}
967
968impl AllureFacade {
969 pub fn with_lifecycle(lifecycle: AllureLifecycle) -> Self {
971 Self {
972 lifecycle: Some(lifecycle),
973 }
974 }
975
976 pub fn set_lifecycle(&mut self, lifecycle: AllureLifecycle) {
978 self.lifecycle = Some(lifecycle);
979 }
980
981 pub(crate) fn lifecycle(&self) -> Option<&AllureLifecycle> {
982 self.lifecycle.as_ref()
983 }
984
985 pub fn start_test_case(&self, params: impl Into<StartTestCaseParams>) {
987 if let Some(l) = &self.lifecycle {
988 l.start_test_case(params);
989 }
990 }
991
992 pub fn stop_test_case(&self, status: Status, details: Option<StatusDetails>) {
994 if let Some(l) = &self.lifecycle {
995 l.stop_test_case(status, details);
996 }
997 }
998
999 pub fn start_test(&self, name: impl Into<String>) {
1001 self.start_test_case(name.into());
1002 }
1003
1004 pub fn start_test_with_full_name(&self, name: impl Into<String>, full_name: impl Into<String>) {
1006 self.start_test_case(StartTestCaseParams::new(name).with_full_name(full_name));
1007 }
1008
1009 pub fn end_test(&self, status: Status, details: Option<StatusDetails>) {
1011 self.stop_test_case(status, details);
1012 }
1013
1014 pub fn description(&self, description: impl Into<String>) {
1016 if let Some(l) = &self.lifecycle {
1017 l.update_test_case(|t| t.description = Some(description.into()));
1018 }
1019 }
1020
1021 pub fn description_html(&self, description: impl Into<String>) {
1023 if let Some(l) = &self.lifecycle {
1024 l.update_test_case(|t| t.description_html = Some(description.into()));
1025 }
1026 }
1027
1028 pub fn display_name(&self, name: impl Into<String>) {
1030 if let Some(l) = &self.lifecycle {
1031 l.update_test_case(|t| t.name = name.into());
1032 }
1033 }
1034
1035 pub fn label(&self, name: impl Into<String>, value: impl Into<String>) {
1037 if let Some(l) = &self.lifecycle {
1038 l.add_label(name, value);
1039 }
1040 }
1041
1042 pub fn labels<I, K, V>(&self, labels: I)
1044 where
1045 I: IntoIterator<Item = (K, V)>,
1046 K: Into<String>,
1047 V: Into<String>,
1048 {
1049 for (k, v) in labels {
1050 self.label(k, v);
1051 }
1052 }
1053
1054 pub fn link(&self, url: impl Into<String>, name: Option<String>, link_type: Option<String>) {
1056 if let Some(l) = &self.lifecycle {
1057 l.add_link(url, name, link_type);
1058 }
1059 }
1060
1061 pub fn links<I, U, N, T>(&self, links: I)
1063 where
1064 I: IntoIterator<Item = (U, Option<N>, Option<T>)>,
1065 U: Into<String>,
1066 N: Into<String>,
1067 T: Into<String>,
1068 {
1069 for (url, name, link_type) in links {
1070 self.link(url, name.map(Into::into), link_type.map(Into::into));
1071 }
1072 }
1073
1074 pub fn parameter(&self, name: impl Into<String>, value: impl Into<String>) {
1076 self.parameter_with_options(name, value, None, None);
1077 }
1078
1079 pub fn parameter_excluded(
1081 &self,
1082 name: impl Into<String>,
1083 value: impl Into<String>,
1084 excluded: bool,
1085 ) {
1086 self.parameter_with_options(name, value, Some(excluded), None);
1087 }
1088
1089 pub fn parameter_mode(
1091 &self,
1092 name: impl Into<String>,
1093 value: impl Into<String>,
1094 mode: ParameterMode,
1095 ) {
1096 self.parameter_with_options(name, value, None, Some(mode));
1097 }
1098
1099 pub fn parameter_with_options(
1101 &self,
1102 name: impl Into<String>,
1103 value: impl Into<String>,
1104 excluded: Option<bool>,
1105 mode: Option<ParameterMode>,
1106 ) {
1107 if let Some(l) = &self.lifecycle {
1108 l.add_parameter_with_options(name, value, excluded, mode);
1109 }
1110 }
1111
1112 pub fn title_path<I, V>(&self, title_path: I)
1114 where
1115 I: IntoIterator<Item = V>,
1116 V: Into<String>,
1117 {
1118 if let Some(l) = &self.lifecycle {
1119 let title_path = title_path.into_iter().map(Into::into).collect();
1120 l.update_test_case(|test| test.title_path = Some(title_path));
1121 }
1122 }
1123
1124 pub fn test_case_id(&self, value: impl Into<String>) {
1126 if let Some(l) = &self.lifecycle {
1127 l.set_test_case_id(value);
1128 }
1129 }
1130
1131 pub fn history_id(&self, value: impl Into<String>) {
1133 if let Some(l) = &self.lifecycle {
1134 l.set_history_id(value);
1135 }
1136 }
1137
1138 pub fn attachment(
1140 &self,
1141 name: impl Into<String>,
1142 content_type: impl Into<String>,
1143 body: impl AsRef<[u8]>,
1144 ) {
1145 let name = name.into();
1146 let content_type = content_type.into();
1147 let body = body.as_ref().to_vec();
1148 self.step(name.clone(), || {
1149 current_owner::add_attachment(self, name, content_type, body);
1150 });
1151 }
1152
1153 pub fn attachment_path(
1155 &self,
1156 name: impl Into<String>,
1157 content_type: impl Into<String>,
1158 path: impl AsRef<Path>,
1159 ) -> std::io::Result<()> {
1160 let bytes = fs::read(path)?;
1161 self.attachment(name, content_type, bytes);
1162 Ok(())
1163 }
1164
1165 pub fn attach_trace(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
1167 self.attach_trace_named("trace.zip", path)
1168 }
1169
1170 pub fn attach_trace_named(
1172 &self,
1173 name: impl Into<String>,
1174 path: impl AsRef<Path>,
1175 ) -> std::io::Result<()> {
1176 self.attachment_path(name, PLAYWRIGHT_TRACE_ATTACHMENT_MIME, path)
1177 }
1178
1179 pub fn global_attachment(
1181 &self,
1182 name: impl Into<String>,
1183 content_type: impl Into<String>,
1184 body: impl AsRef<[u8]>,
1185 ) -> std::io::Result<()> {
1186 let name = name.into();
1187 let content_type = content_type.into();
1188 if let Some(l) = &self.lifecycle {
1189 return l.add_global_attachment(name, content_type, body.as_ref());
1190 }
1191
1192 let writer = FileSystemResultsWriter::from_env()?;
1193 let (source, _) = writer.write_attachment_auto(
1194 &facade_id(),
1195 Some(&name),
1196 Some(&content_type),
1197 body.as_ref(),
1198 )?;
1199 write_globals(Globals {
1200 attachments: vec![GlobalAttachment {
1201 name,
1202 source,
1203 content_type,
1204 }],
1205 errors: Vec::new(),
1206 })
1207 }
1208
1209 pub fn global_attachment_path(
1211 &self,
1212 name: impl Into<String>,
1213 content_type: impl Into<String>,
1214 path: impl AsRef<Path>,
1215 ) -> std::io::Result<()> {
1216 let bytes = fs::read(path)?;
1217 self.global_attachment(name, content_type, bytes)
1218 }
1219
1220 pub fn global_error(&self, message: impl Into<String>) -> std::io::Result<()> {
1222 let message = message.into();
1223 if let Some(l) = &self.lifecycle {
1224 return l.add_global_error(message, None);
1225 }
1226
1227 write_globals(Globals {
1228 attachments: Vec::new(),
1229 errors: vec![GlobalError {
1230 message,
1231 trace: None,
1232 }],
1233 })
1234 }
1235
1236 pub fn global_error_with_trace(
1238 &self,
1239 message: impl Into<String>,
1240 trace: impl Into<String>,
1241 ) -> std::io::Result<()> {
1242 let message = message.into();
1243 let trace = trace.into();
1244 if let Some(l) = &self.lifecycle {
1245 return l.add_global_error(message, Some(trace));
1246 }
1247
1248 write_globals(Globals {
1249 attachments: Vec::new(),
1250 errors: vec![GlobalError {
1251 message,
1252 trace: Some(trace),
1253 }],
1254 })
1255 }
1256
1257 pub fn http_exchange(&self, exchange: HttpExchange) {
1259 self.http_exchange_named(HTTP_EXCHANGE_ATTACHMENT_NAME, exchange);
1260 }
1261
1262 pub fn http_exchange_named(&self, name: impl Into<String>, exchange: HttpExchange) {
1264 let name = name.into();
1265 self.step(name.clone(), || {
1266 current_owner::add_http_exchange_named(self, name, exchange);
1267 });
1268 }
1269
1270 pub fn enter_step(&self, name: impl Into<String>) -> StepGuard {
1278 if let Some(l) = &self.lifecycle {
1279 l.start_step(name);
1280 }
1281 StepGuard {
1282 allure: self.clone(),
1283 status: Status::Passed,
1284 details: None,
1285 finished: false,
1286 }
1287 }
1288
1289 fn start_step_scope(&self, name: impl Into<String>) -> StepScope {
1290 if let Some(l) = &self.lifecycle {
1291 l.start_step(name);
1292 StepScope {
1293 lifecycle: self.lifecycle.clone(),
1294 status: Some(Status::Passed),
1295 details: None,
1296 }
1297 } else {
1298 StepScope {
1299 lifecycle: None,
1300 status: None,
1301 details: None,
1302 }
1303 }
1304 }
1305
1306 pub fn step<T, F>(&self, name: impl Into<String>, body: F) -> T
1308 where
1309 F: FnOnce() -> T,
1310 {
1311 let guard = self.start_step_scope(name);
1312 let outcome = panic::catch_unwind(AssertUnwindSafe(body));
1313 match outcome {
1314 Ok(value) => {
1315 drop(guard);
1316 value
1317 }
1318 Err(payload) => {
1319 let (status, details) = error_classifier::classify_panic(&payload);
1320 drop(guard.with_status(status, Some(details)));
1321 panic::resume_unwind(payload);
1322 }
1323 }
1324 }
1325
1326 pub fn stage(&self, name: impl Into<String>) {
1328 if let Some(l) = &self.lifecycle {
1329 l.start_stage(name);
1330 }
1331 }
1332
1333 pub fn log_step(&self, name: impl Into<String>) {
1335 self.log_step_with(name, None, None::<String>);
1336 }
1337
1338 pub fn log_step_with<E>(
1340 &self,
1341 name: impl Into<String>,
1342 status: Option<Status>,
1343 error: Option<E>,
1344 ) where
1345 E: ToString,
1346 {
1347 if let Some(l) = &self.lifecycle {
1348 let timestamp = l.start_step_at(name, None);
1349 let status = status.unwrap_or(Status::Passed);
1350 let details = error.map(|error| StatusDetails {
1351 message: Some(error.to_string()),
1352 trace: None,
1353 actual: None,
1354 expected: None,
1355 });
1356 l.stop_step_at(Some(timestamp), status, details);
1357 }
1358 }
1359
1360 pub fn issue(&self, name: impl Into<String>, url: impl Into<String>) {
1362 self.link(url.into(), Some(name.into()), Some("issue".to_string()));
1363 }
1364
1365 pub fn tms(&self, name: impl Into<String>, url: impl Into<String>) {
1367 self.link(url.into(), Some(name.into()), Some("tms".to_string()));
1368 }
1369
1370 pub fn epic(&self, value: impl Into<String>) {
1372 self.label("epic", value);
1373 }
1374 pub fn feature(&self, value: impl Into<String>) {
1376 self.label("feature", value);
1377 }
1378 pub fn story(&self, value: impl Into<String>) {
1380 self.label("story", value);
1381 }
1382 pub fn suite(&self, value: impl Into<String>) {
1384 self.label("suite", value);
1385 }
1386 pub fn parent_suite(&self, value: impl Into<String>) {
1388 self.label("parentSuite", value);
1389 }
1390 pub fn sub_suite(&self, value: impl Into<String>) {
1392 self.label("subSuite", value);
1393 }
1394 pub fn owner(&self, value: impl Into<String>) {
1396 self.label("owner", value);
1397 }
1398 pub fn severity(&self, value: impl Into<String>) {
1400 self.label("severity", value);
1401 }
1402 pub fn layer(&self, value: impl Into<String>) {
1404 self.label("layer", value);
1405 }
1406 pub fn tag(&self, value: impl Into<String>) {
1408 self.label("tag", value);
1409 }
1410 pub fn tags<I, V>(&self, tags: I)
1412 where
1413 I: IntoIterator<Item = V>,
1414 V: Into<String>,
1415 {
1416 for tag in tags {
1417 self.tag(tag);
1418 }
1419 }
1420 pub fn id(&self, value: impl Into<String>) {
1422 self.label("ALLURE_ID", value);
1423 }
1424 pub fn allure_id(&self, value: impl Into<String>) {
1426 self.id(value);
1427 }
1428}
1429
1430struct StepScope {
1431 lifecycle: Option<AllureLifecycle>,
1432 status: Option<Status>,
1433 details: Option<StatusDetails>,
1434}
1435
1436impl StepScope {
1437 fn with_status(mut self, status: Status, details: Option<StatusDetails>) -> Self {
1438 self.status = Some(status);
1439 self.details = details;
1440 self
1441 }
1442}
1443
1444impl Drop for StepScope {
1445 fn drop(&mut self) {
1446 if let (Some(l), Some(status)) = (&self.lifecycle, self.status.clone()) {
1447 l.stop_step(status, self.details.clone());
1448 }
1449 }
1450}
1451
1452pub struct StepGuard {
1458 allure: AllureFacade,
1459 status: Status,
1460 details: Option<StatusDetails>,
1461 finished: bool,
1462}
1463
1464impl StepGuard {
1465 pub fn set_status(&mut self, status: Status, details: Option<StatusDetails>) {
1467 self.status = status;
1468 self.details = details;
1469 }
1470
1471 pub fn fail(&mut self, message: impl Into<String>) {
1473 self.set_status(
1474 Status::Failed,
1475 Some(StatusDetails {
1476 message: Some(message.into()),
1477 trace: None,
1478 actual: None,
1479 expected: None,
1480 }),
1481 );
1482 }
1483
1484 pub fn finish(mut self) {
1486 self.close();
1487 }
1488
1489 fn close(&mut self) {
1490 if self.finished {
1491 return;
1492 }
1493 self.finished = true;
1494 if let Some(l) = self.allure.lifecycle() {
1495 let status = if std::thread::panicking() && matches!(self.status, Status::Passed) {
1496 Status::Broken
1497 } else {
1498 self.status.clone()
1499 };
1500 l.stop_step(status, self.details.take());
1501 }
1502 }
1503}
1504
1505impl Drop for StepGuard {
1506 fn drop(&mut self) {
1507 self.close();
1508 }
1509}
1510
1511#[doc(hidden)]
1512pub mod __private {
1513 use super::*;
1514
1515 pub struct StepScope {
1516 allure: AllureFacade,
1517 status: Status,
1518 details: Option<StatusDetails>,
1519 }
1520
1521 pub fn begin_step_scope(allure: AllureFacade, name: impl Into<String>) -> StepScope {
1522 if let Some(lifecycle) = &allure.lifecycle {
1523 lifecycle.start_step(name);
1524 }
1525 StepScope {
1526 allure,
1527 status: Status::Passed,
1528 details: None,
1529 }
1530 }
1531
1532 impl Drop for StepScope {
1533 fn drop(&mut self) {
1534 let status = if std::thread::panicking() && matches!(self.status, Status::Passed) {
1535 Status::Broken
1536 } else {
1537 self.status.clone()
1538 };
1539 if let Some(lifecycle) = &self.allure.lifecycle {
1540 lifecycle.stop_step(status, self.details.take());
1541 }
1542 }
1543 }
1544}
1545
1546#[cfg(test)]
1547#[path = "facade_tests.rs"]
1548mod tests;