Skip to main content

allure_rust_commons/
facade.rs

1//! Framework-neutral high-level Allure runtime facade.
2
3use 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
40/// Returns the process-wide default facade.
41pub fn allure() -> &'static AllureFacade {
42    ALLURE.get_or_init(AllureFacade::default)
43}
44
45/// Guard that restores the previous thread-bound facade when dropped.
46pub struct CurrentAllureGuard {
47    previous: Option<AllureFacade>,
48}
49
50/// Binds an Allure facade to the current thread.
51pub fn push_current_allure(allure: &AllureFacade) -> CurrentAllureGuard {
52    let previous = CURRENT_ALLURE.with(|current| current.replace(Some(allure.clone())));
53    CurrentAllureGuard { previous }
54}
55
56/// Returns the facade currently bound to this thread.
57pub fn current_allure() -> Option<AllureFacade> {
58    CURRENT_ALLURE.with(|current| current.borrow().clone())
59}
60
61/// Clears pending assertion failure details captured by assertion logging.
62pub fn clear_last_assertion_failure() {
63    LAST_ASSERTION_FAILURE.with(|failure| {
64        failure.replace(None);
65    });
66}
67
68/// Returns status details for a failure message, reusing captured assertion details when present.
69pub 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
81/// Records a passed assertion as a log step on the current facade.
82pub 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]
89/// Records a failed assertion and panics with the original assertion message.
90pub 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/// Options used by the macro-free `test` runtime wrappers.
133#[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    /// Creates options for a test with the given display name.
146    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    /// Infers options from the current cargo-test thread name.
159    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    /// Creates options from low-level lifecycle start parameters.
166    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    /// Sets the test full name.
179    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    /// Sets an explicit history identifier.
185    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    /// Sets an explicit test case identifier.
191    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    /// Sets the markdown description.
197    pub fn with_description(mut self, description: impl Into<String>) -> Self {
198        self.params.description = Some(description.into());
199        self
200    }
201
202    /// Sets the HTML description.
203    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    /// Adds an initial label.
209    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    /// Adds an initial Allure ID label.
218    pub fn with_allure_id(self, value: impl Into<String>) -> Self {
219        self.with_label("ALLURE_ID", value)
220    }
221
222    /// Sets the Cargo manifest directory used for package metadata lookup.
223    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    /// Sets the Rust module path used for package metadata lookup.
229    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    /// Sets the title path.
235    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    /// Sets source-location metadata used for title path and package metadata labels.
245    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    /// Sets the framework label value.
259    pub fn with_framework(mut self, framework: impl Into<String>) -> Self {
260        self.framework = Some(framework.into());
261        self
262    }
263
264    /// Disables the framework label.
265    pub fn without_framework(mut self) -> Self {
266        self.framework = None;
267        self
268    }
269
270    /// Disables synthetic suite labels.
271    pub fn without_synthetic_suite_labels(mut self) -> Self {
272        self.synthetic_suite_labels = false;
273        self
274    }
275
276    /// Sets the status to use for unclassified panics.
277    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
289/// Reports a Rust test return value as an Allure outcome.
290///
291/// This mirrors the return shapes accepted by modern Rust test functions without consuming the
292/// value before the test harness receives it.
293pub trait AllureTestOutcome: Sized {
294    /// Returns a successful value used when the test is filtered out before execution.
295    fn successful() -> Self;
296
297    /// Returns the Allure status represented by this value.
298    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/// Outcome produced after interpreting a Rust test return value.
349#[doc(hidden)]
350pub struct AllureOutcomeReport {
351    /// Exit code that should be returned to Rust's test harness.
352    pub exit_code: ExitCode,
353    /// Allure status represented by the return value.
354    pub status: Status,
355    /// Optional status details represented by the return value.
356    pub details: Option<StatusDetails>,
357}
358
359/// Concrete return-value probe used by generated test wrappers.
360#[doc(hidden)]
361pub struct AllureOutcomeProbe<R> {
362    value: Option<R>,
363}
364
365impl<R> AllureOutcomeProbe<R> {
366    /// Creates a probe for a concrete Rust test return value.
367    pub fn new(value: R) -> Self {
368        Self { value: Some(value) }
369    }
370}
371
372/// Finishes an [`AllureOutcomeProbe`] using the most specific known outcome contract.
373#[doc(hidden)]
374pub trait FinishAllureOutcome {
375    /// Reports the probed value to Rust's test harness and returns the matching Allure outcome.
376    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
432/// Runs a synchronous test body with inferred Allure metadata.
433pub fn test<R, F>(body: F) -> R
434where
435    F: FnOnce() -> R,
436{
437    test_with(TestOptions::inferred(), body)
438}
439
440/// Runs a synchronous test body with a custom display name.
441pub 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
449/// Runs a synchronous test body with explicit options.
450pub 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
460/// Runs a synchronous test body and reports Result-like return values as Allure outcomes.
461pub 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
472/// Runs an async test future with inferred Allure metadata.
473pub 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
480/// Runs an async test future with a custom display name.
481pub 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
489/// Runs an async test future with explicit options.
490pub 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
503/// Runs an async test future and reports Result-like return values as Allure outcomes.
504pub 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/// In-progress runtime test state used by generated adapter code.
519#[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/// Starts a runtime test with explicit options for generated adapter code.
565#[doc(hidden)]
566pub fn start_runtime_test_with_options(options: impl Into<TestOptions>) -> RuntimeTest {
567    start_runtime_test(options.into())
568}
569
570/// Pushes the current Allure context for generated adapter code.
571#[doc(hidden)]
572pub fn push_runtime_test_allure(runtime_test: &RuntimeTest) -> CurrentAllureGuard {
573    push_current_allure(&runtime_test.allure)
574}
575
576/// Polls a future with the runtime test's Allure context installed.
577#[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/// Finishes a runtime test with an explicit status for generated adapter code.
593#[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/// Finishes a runtime test for a panic and resumes unwinding.
603#[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
680/// Sets the markdown description on the current test.
681pub fn description(description: impl Into<String>) {
682    active_allure().description(description);
683}
684
685/// Sets the HTML description on the current test.
686pub fn description_html(description: impl Into<String>) {
687    active_allure().description_html(description);
688}
689
690/// Sets the display name of the current test.
691pub fn display_name(name: impl Into<String>) {
692    active_allure().display_name(name);
693}
694
695/// Adds a label to the current test.
696pub fn label(name: impl Into<String>, value: impl Into<String>) {
697    active_allure().label(name, value);
698}
699
700/// Adds multiple labels to the current test.
701pub 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
710/// Adds a link to the current test.
711pub 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
720/// Adds multiple links to the current test.
721pub 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
731/// Adds a parameter to the current test.
732pub fn parameter(name: impl Into<String>, value: impl Into<String>) {
733    active_allure().parameter(name, value);
734}
735
736/// Adds a parameter and controls whether it participates in history identity.
737pub fn parameter_excluded(name: impl Into<String>, value: impl Into<String>, excluded: bool) {
738    active_allure().parameter_excluded(name, value, excluded);
739}
740
741/// Adds a parameter with a display mode.
742pub fn parameter_mode(name: impl Into<String>, value: impl Into<String>, mode: ParameterMode) {
743    active_allure().parameter_mode(name, value, mode);
744}
745
746/// Adds a parameter with identity and display options.
747pub 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
756/// Sets the current test title path.
757pub 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
765/// Sets the current test case identifier.
766pub fn test_case_id(value: impl Into<String>) {
767    active_allure().test_case_id(value);
768}
769
770/// Sets the current test history identifier.
771pub fn history_id(value: impl Into<String>) {
772    active_allure().history_id(value);
773}
774
775/// Adds an attachment as an ordered evidence step.
776pub 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
784/// Adds a file attachment as an ordered evidence step.
785pub 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
793/// Enters a manually-scoped step on the current thread-bound facade.
794pub fn enter_step(name: impl Into<String>) -> StepGuard {
795    active_allure().enter_step(name)
796}
797
798/// Adds a Playwright trace attachment named `trace.zip`.
799pub fn attach_trace(path: impl AsRef<Path>) -> std::io::Result<()> {
800    active_allure().attach_trace(path)
801}
802
803/// Adds a named Playwright trace attachment.
804pub 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
808/// Adds a run-level attachment.
809pub 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
817/// Adds a run-level file attachment.
818pub 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
826/// Adds a run-level error.
827pub fn global_error(message: impl Into<String>) -> std::io::Result<()> {
828    active_allure().global_error(message)
829}
830
831/// Adds a run-level error with a trace.
832pub 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
839/// Adds an HTTP exchange as an ordered evidence step.
840pub fn http_exchange(exchange: HttpExchange) {
841    active_allure().http_exchange(exchange);
842}
843
844/// Adds a named HTTP exchange as an ordered evidence step.
845pub fn http_exchange_named(name: impl Into<String>, exchange: HttpExchange) {
846    active_allure().http_exchange_named(name, exchange);
847}
848
849/// Runs a lambda-style step.
850pub 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
857/// Starts a semantic stage under the current owner.
858pub fn stage(name: impl Into<String>) {
859    active_allure().stage(name);
860}
861
862/// Records a passed log step.
863pub fn log_step(name: impl Into<String>) {
864    active_allure().log_step(name);
865}
866
867/// Records an instant log step with optional status and error details.
868pub 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
875/// Adds an issue link to the current test.
876pub fn issue(name: impl Into<String>, url: impl Into<String>) {
877    active_allure().issue(name, url);
878}
879
880/// Adds a test-management-system link to the current test.
881pub fn tms(name: impl Into<String>, url: impl Into<String>) {
882    active_allure().tms(name, url);
883}
884
885/// Adds an `epic` label.
886pub fn epic(value: impl Into<String>) {
887    active_allure().epic(value);
888}
889
890/// Adds a `feature` label.
891pub fn feature(value: impl Into<String>) {
892    active_allure().feature(value);
893}
894
895/// Adds a `story` label.
896pub fn story(value: impl Into<String>) {
897    active_allure().story(value);
898}
899
900/// Sets the `suite` label.
901pub fn suite(value: impl Into<String>) {
902    active_allure().suite(value);
903}
904
905/// Sets the `parentSuite` label.
906pub fn parent_suite(value: impl Into<String>) {
907    active_allure().parent_suite(value);
908}
909
910/// Sets the `subSuite` label.
911pub fn sub_suite(value: impl Into<String>) {
912    active_allure().sub_suite(value);
913}
914
915/// Adds an `owner` label.
916pub fn owner(value: impl Into<String>) {
917    active_allure().owner(value);
918}
919
920/// Adds a `severity` label.
921pub fn severity(value: impl Into<String>) {
922    active_allure().severity(value);
923}
924
925/// Adds a `layer` label.
926pub fn layer(value: impl Into<String>) {
927    active_allure().layer(value);
928}
929
930/// Adds a `tag` label.
931pub fn tag(value: impl Into<String>) {
932    active_allure().tag(value);
933}
934
935/// Adds multiple `tag` labels.
936pub fn tags<I, V>(tags: I)
937where
938    I: IntoIterator<Item = V>,
939    V: Into<String>,
940{
941    active_allure().tags(tags);
942}
943
944/// Adds the Allure ID label.
945pub fn id(value: impl Into<String>) {
946    active_allure().id(value);
947}
948
949/// Adds the Allure ID label.
950pub 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/// Explicit Allure runtime facade.
963#[derive(Clone, Default)]
964pub struct AllureFacade {
965    lifecycle: Option<AllureLifecycle>,
966}
967
968impl AllureFacade {
969    /// Creates a facade backed by a lifecycle owner.
970    pub fn with_lifecycle(lifecycle: AllureLifecycle) -> Self {
971        Self {
972            lifecycle: Some(lifecycle),
973        }
974    }
975
976    /// Replaces the lifecycle owner used by this facade.
977    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    /// Starts a test case through the underlying lifecycle.
986    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    /// Stops the current test case through the underlying lifecycle.
993    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    /// Starts a test with a display name.
1000    pub fn start_test(&self, name: impl Into<String>) {
1001        self.start_test_case(name.into());
1002    }
1003
1004    /// Starts a test with a display name and full name.
1005    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    /// Ends the current test with a final status.
1010    pub fn end_test(&self, status: Status, details: Option<StatusDetails>) {
1011        self.stop_test_case(status, details);
1012    }
1013
1014    /// Sets the markdown description on the current test.
1015    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    /// Sets the HTML description on the current test.
1022    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    /// Sets the display name of the current test.
1029    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    /// Adds a label to the current test.
1036    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    /// Adds multiple labels to the current test.
1043    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    /// Adds a link to the current test.
1055    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    /// Adds multiple links to the current test.
1062    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    /// Adds a parameter to the current test.
1075    pub fn parameter(&self, name: impl Into<String>, value: impl Into<String>) {
1076        self.parameter_with_options(name, value, None, None);
1077    }
1078
1079    /// Adds a parameter and controls whether it participates in history identity.
1080    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    /// Adds a parameter with a display mode.
1090    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    /// Adds a parameter with identity and display options.
1100    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    /// Sets the current test title path.
1113    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    /// Sets the current test case identifier.
1125    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    /// Sets the current test history identifier.
1132    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    /// Adds an attachment as an ordered evidence step.
1139    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    /// Adds a file attachment as an ordered evidence step.
1154    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    /// Adds a Playwright trace attachment named `trace.zip`.
1166    pub fn attach_trace(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
1167        self.attach_trace_named("trace.zip", path)
1168    }
1169
1170    /// Adds a named Playwright trace attachment.
1171    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    /// Adds a run-level attachment.
1180    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    /// Adds a run-level file attachment.
1210    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    /// Adds a run-level error.
1221    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    /// Adds a run-level error with a trace.
1237    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    /// Adds an HTTP exchange as an ordered evidence step.
1258    pub fn http_exchange(&self, exchange: HttpExchange) {
1259        self.http_exchange_named(HTTP_EXCHANGE_ATTACHMENT_NAME, exchange);
1260    }
1261
1262    /// Adds a named HTTP exchange as an ordered evidence step.
1263    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    /// Enters a step that stays open until the returned [`StepGuard`] is dropped.
1271    ///
1272    /// Unlike [`AllureFacade::step`], the step is not tied to a closure, so adapters can
1273    /// bracket work that spans separate start and finish callbacks (such as Diesel's
1274    /// `StartQuery`/`FinishQuery` instrumentation events) and record a failing status without
1275    /// panicking. The guard defaults to [`Status::Passed`]; use [`StepGuard::fail`] or
1276    /// [`StepGuard::set_status`] before it drops to record a different outcome.
1277    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    /// Runs a lambda-style step.
1307    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    /// Starts a semantic stage under the current owner.
1327    pub fn stage(&self, name: impl Into<String>) {
1328        if let Some(l) = &self.lifecycle {
1329            l.start_stage(name);
1330        }
1331    }
1332
1333    /// Records a passed log step.
1334    pub fn log_step(&self, name: impl Into<String>) {
1335        self.log_step_with(name, None, None::<String>);
1336    }
1337
1338    /// Records an instant log step with optional status and error details.
1339    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    /// Adds an issue link to the current test.
1361    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    /// Adds a test-management-system link to the current test.
1366    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    /// Adds an `epic` label.
1371    pub fn epic(&self, value: impl Into<String>) {
1372        self.label("epic", value);
1373    }
1374    /// Adds a `feature` label.
1375    pub fn feature(&self, value: impl Into<String>) {
1376        self.label("feature", value);
1377    }
1378    /// Adds a `story` label.
1379    pub fn story(&self, value: impl Into<String>) {
1380        self.label("story", value);
1381    }
1382    /// Sets the `suite` label.
1383    pub fn suite(&self, value: impl Into<String>) {
1384        self.label("suite", value);
1385    }
1386    /// Sets the `parentSuite` label.
1387    pub fn parent_suite(&self, value: impl Into<String>) {
1388        self.label("parentSuite", value);
1389    }
1390    /// Sets the `subSuite` label.
1391    pub fn sub_suite(&self, value: impl Into<String>) {
1392        self.label("subSuite", value);
1393    }
1394    /// Adds an `owner` label.
1395    pub fn owner(&self, value: impl Into<String>) {
1396        self.label("owner", value);
1397    }
1398    /// Adds a `severity` label.
1399    pub fn severity(&self, value: impl Into<String>) {
1400        self.label("severity", value);
1401    }
1402    /// Adds a `layer` label.
1403    pub fn layer(&self, value: impl Into<String>) {
1404        self.label("layer", value);
1405    }
1406    /// Adds a `tag` label.
1407    pub fn tag(&self, value: impl Into<String>) {
1408        self.label("tag", value);
1409    }
1410    /// Adds multiple `tag` labels.
1411    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    /// Adds the Allure ID label.
1421    pub fn id(&self, value: impl Into<String>) {
1422        self.label("ALLURE_ID", value);
1423    }
1424    /// Adds the Allure ID label.
1425    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
1452/// Guard for a manually-scoped Allure step entered with [`AllureFacade::enter_step`].
1453///
1454/// The step stays open until the guard is dropped (or [`StepGuard::finish`] is called). The
1455/// recorded status defaults to [`Status::Passed`]; if the thread is panicking while a passed
1456/// guard drops, the step is recorded as [`Status::Broken`] instead.
1457pub struct StepGuard {
1458    allure: AllureFacade,
1459    status: Status,
1460    details: Option<StatusDetails>,
1461    finished: bool,
1462}
1463
1464impl StepGuard {
1465    /// Sets the status and details recorded when the guard closes the step.
1466    pub fn set_status(&mut self, status: Status, details: Option<StatusDetails>) {
1467        self.status = status;
1468        self.details = details;
1469    }
1470
1471    /// Marks the step failed with the given message.
1472    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    /// Closes the step immediately with the currently recorded status.
1485    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;