1use std::{
4 cell::RefCell,
5 cmp,
6 collections::HashMap,
7 sync::{
8 atomic::{AtomicU64, Ordering},
9 Arc, Mutex,
10 },
11 time::{SystemTime, UNIX_EPOCH},
12};
13
14use crate::{
15 config,
16 http_exchange::{HttpExchange, HTTP_EXCHANGE_ATTACHMENT_MIME, HTTP_EXCHANGE_ATTACHMENT_NAME},
17 md5::md5_hex,
18 model::{
19 Attachment, FixtureResult, GlobalAttachment, GlobalError, Globals, Label, Link, Parameter,
20 ParameterMode, Stage, Status, StatusDetails, StepResult, TestResult, TestResultContainer,
21 },
22 writer::FileSystemResultsWriter,
23};
24
25thread_local! {
26 static ACTIVE_TEST_ROOT: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
27 static ACTIVE_SCOPE_ROOT: RefCell<Option<String>> = const { RefCell::new(None) };
28}
29
30static ID_COUNTER: AtomicU64 = AtomicU64::new(1);
31
32fn now_millis() -> i64 {
33 SystemTime::now()
34 .duration_since(UNIX_EPOCH)
35 .map(|d| d.as_millis() as i64)
36 .unwrap_or_default()
37}
38
39fn next_id() -> String {
40 format!(
41 "{}-{}",
42 now_millis(),
43 ID_COUNTER.fetch_add(1, Ordering::Relaxed)
44 )
45}
46
47fn round_millis(value: f64) -> i64 {
48 value.round() as i64
49}
50
51fn normalize_times(
52 start: Option<i64>,
53 stop: Option<i64>,
54 duration: Option<f64>,
55 fallback_stop: i64,
56) -> (Option<i64>, Option<i64>) {
57 let rounded_duration = duration.map(round_millis).map(|value| cmp::max(value, 0));
58
59 let (start, stop) = match (start, stop, rounded_duration) {
60 (Some(start), Some(stop), _) => (start, cmp::max(stop, start)),
61 (Some(start), None, Some(duration)) => (start, start.saturating_add(duration)),
62 (None, Some(stop), Some(duration)) => (stop.saturating_sub(duration), stop),
63 (Some(start), None, None) => (start, cmp::max(fallback_stop, start)),
64 (None, Some(stop), None) => (stop, stop),
65 (None, None, Some(duration)) => {
66 let stop = fallback_stop;
67 (stop.saturating_sub(duration), stop)
68 }
69 (None, None, None) => (fallback_stop, fallback_stop),
70 };
71
72 (Some(start), Some(stop))
73}
74
75fn normalize_step_result(step: &mut StepResult, fallback_stop: i64) {
76 (step.start, step.stop) = normalize_times(step.start, step.stop, None, fallback_stop);
77 for nested in &mut step.steps {
78 normalize_step_result(nested, step.stop.unwrap_or(fallback_stop));
79 }
80}
81
82fn normalize_fixture_result(fixture: &mut FixtureResult, fallback_stop: i64) {
83 (fixture.start, fixture.stop) =
84 normalize_times(fixture.start, fixture.stop, None, fallback_stop);
85 let fixture_stop = fixture.stop.unwrap_or(fallback_stop);
86 for step in &mut fixture.steps {
87 normalize_step_result(step, fixture_stop);
88 }
89}
90
91fn normalize_test_result(test: &mut TestResult, fallback_stop: i64) {
92 (test.start, test.stop) = normalize_times(test.start, test.stop, None, fallback_stop);
93 let test_stop = test.stop.unwrap_or(fallback_stop);
94 for step in &mut test.steps {
95 normalize_step_result(step, test_stop);
96 }
97}
98
99fn normalize_container_times(container: &mut TestResultContainer, fallback_stop: i64) {
100 (container.start, container.stop) =
101 normalize_times(container.start, container.stop, None, fallback_stop);
102 let container_stop = container.stop.unwrap_or(fallback_stop);
103 for fixture in &mut container.befores {
104 normalize_fixture_result(fixture, container_stop);
105 }
106 for fixture in &mut container.afters {
107 normalize_fixture_result(fixture, container_stop);
108 }
109}
110
111fn derive_test_case_id(test: &TestResult) -> Option<String> {
112 test.test_case_id
113 .clone()
114 .or_else(|| test.full_name.clone().map(|full_name| md5_hex(&full_name)))
115}
116
117fn derive_history_id(test: &TestResult) -> Option<String> {
118 let base = test
119 .test_case_id
120 .as_ref()
121 .or(test.full_name.as_ref())
122 .or(Some(&test.name))?;
123
124 let mut parameters = test
125 .parameters
126 .iter()
127 .filter(|parameter| parameter.excluded != Some(true))
128 .map(|parameter| format!("{}:{}", parameter.name, parameter.value))
129 .collect::<Vec<_>>();
130 parameters.sort();
131 let parameter_hash = md5_hex(¶meters.join(","));
132
133 Some(md5_hex(&format!("{base}:{parameter_hash}")))
134}
135
136#[derive(Clone)]
138pub struct AllureRuntime {
139 writer: Arc<FileSystemResultsWriter>,
140}
141
142impl AllureRuntime {
143 pub fn new(writer: FileSystemResultsWriter) -> Self {
145 Self {
146 writer: Arc::new(writer),
147 }
148 }
149
150 pub fn lifecycle(&self) -> AllureLifecycle {
152 AllureLifecycle {
153 writer: Arc::clone(&self.writer),
154 state: Arc::new(Mutex::new(LifecycleState::default())),
155 }
156 }
157}
158
159#[derive(Clone)]
161pub struct AllureLifecycle {
162 writer: Arc<FileSystemResultsWriter>,
163 state: Arc<Mutex<LifecycleState>>,
164}
165
166#[derive(Debug, Clone, Default)]
168pub struct StartTestCaseParams {
169 pub uuid: Option<String>,
171 pub name: String,
173 pub full_name: Option<String>,
175 pub history_id: Option<String>,
177 pub test_case_id: Option<String>,
179 pub description: Option<String>,
181 pub description_html: Option<String>,
183 pub status: Option<Status>,
185 pub status_details: Option<StatusDetails>,
187 pub stage: Option<Stage>,
189 pub labels: Vec<Label>,
191 pub links: Vec<Link>,
193 pub parameters: Vec<Parameter>,
195 pub steps: Vec<StepResult>,
197 pub attachments: Vec<Attachment>,
199 pub title_path: Option<Vec<String>>,
201 pub start: Option<i64>,
203 pub stop: Option<i64>,
205}
206
207impl StartTestCaseParams {
208 pub fn new(name: impl Into<String>) -> Self {
210 Self {
211 name: name.into(),
212 ..Default::default()
213 }
214 }
215
216 pub fn with_full_name(mut self, full_name: impl Into<String>) -> Self {
218 self.full_name = Some(full_name.into());
219 self
220 }
221}
222
223impl From<String> for StartTestCaseParams {
224 fn from(name: String) -> Self {
225 Self {
226 name,
227 ..Default::default()
228 }
229 }
230}
231
232impl From<&str> for StartTestCaseParams {
233 fn from(name: &str) -> Self {
234 Self::from(name.to_string())
235 }
236}
237
238#[derive(Default)]
239struct LifecycleState {
240 tests: HashMap<String, TestState>,
241 scopes: HashMap<String, ScopeState>,
242}
243
244struct TestState {
245 test: TestResult,
246 step_stack: Vec<RunningStep>,
247 linked_scopes: Vec<String>,
248}
249
250struct ScopeState {
251 container: TestResultContainer,
252 running_fixture: Option<RunningFixture>,
253}
254
255struct RunningFixture {
256 kind: FixtureKind,
257 fixture: FixtureResult,
258 step_stack: Vec<RunningStep>,
259}
260
261enum FixtureKind {
262 Before,
263 After,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267enum RunningStepKind {
268 Step,
269 Stage,
270}
271
272struct RunningStep {
273 result: StepResult,
274 kind: RunningStepKind,
275}
276
277impl RunningStep {
278 fn new(name: impl Into<String>, timestamp: i64, kind: RunningStepKind) -> Self {
279 Self {
280 result: StepResult {
281 name: name.into(),
282 stage: Some(Stage::Running),
283 start: Some(timestamp),
284 ..Default::default()
285 },
286 kind,
287 }
288 }
289}
290
291impl std::ops::Deref for RunningStep {
292 type Target = StepResult;
293
294 fn deref(&self) -> &Self::Target {
295 &self.result
296 }
297}
298
299impl std::ops::DerefMut for RunningStep {
300 fn deref_mut(&mut self) -> &mut Self::Target {
301 &mut self.result
302 }
303}
304
305impl AllureLifecycle {
306 pub fn start_test_case(&self, params: impl Into<StartTestCaseParams>) {
308 let params = params.into();
309 let name = params.name;
310 let uuid = params.uuid.unwrap_or_else(next_id);
311 let full_name = params.full_name.or_else(|| Some(name.clone()));
312 let mut labels = config::global_labels_from_environment()
313 .into_iter()
314 .map(|(name, value)| Label { name, value })
315 .collect::<Vec<_>>();
316 labels.extend(params.labels);
317
318 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
319 lock.tests.insert(
320 uuid.clone(),
321 TestState {
322 test: TestResult {
323 uuid: uuid.clone(),
324 name,
325 full_name,
326 history_id: params.history_id,
327 test_case_id: params.test_case_id,
328 description: params.description,
329 description_html: params.description_html,
330 status: params.status,
331 status_details: params.status_details,
332 stage: params.stage.or(Some(Stage::Running)),
333 labels,
334 links: params.links,
335 parameters: params.parameters,
336 steps: params.steps,
337 attachments: params.attachments,
338 title_path: params.title_path,
339 start: params.start.or_else(|| Some(now_millis())),
340 stop: params.stop,
341 },
342 step_stack: Vec::new(),
343 linked_scopes: Vec::new(),
344 },
345 );
346 ACTIVE_TEST_ROOT.with(|cell| cell.borrow_mut().push(uuid));
347 }
348
349 pub fn current_test_uuid(&self) -> Option<String> {
351 ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned())
352 }
353
354 pub fn stop_test_case(&self, status: Status, details: Option<StatusDetails>) {
356 let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) else {
357 return;
358 };
359
360 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
361 if let Some(mut state) = lock.tests.remove(&test_uuid) {
362 finalize_steps(
363 &mut state.step_stack,
364 &mut state.test.steps,
365 status.clone(),
366 details.clone(),
367 );
368 merge_before_scope_metadata(&lock, &mut state.test, &state.linked_scopes);
369
370 state.test.status = Some(status);
371 state.test.status_details = details;
372 state.test.stage = Some(Stage::Finished);
373 let fallback_stop = now_millis();
374 if state.test.test_case_id.is_none() {
375 state.test.test_case_id = derive_test_case_id(&state.test);
376 }
377 if state.test.history_id.is_none() {
378 state.test.history_id = derive_history_id(&state.test);
379 }
380 normalize_test_result(&mut state.test, fallback_stop);
381 let _ = self.writer.write_result(&state.test);
382 }
383
384 ACTIVE_TEST_ROOT.with(|cell| {
385 let mut roots = cell.borrow_mut();
386 if roots.last().is_some_and(|uuid| uuid == &test_uuid) {
387 roots.pop();
388 } else {
389 roots.retain(|uuid| uuid != &test_uuid);
390 }
391 });
392 }
393
394 pub fn update_test_case<F>(&self, update: F)
396 where
397 F: FnOnce(&mut TestResult),
398 {
399 let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) else {
400 return;
401 };
402 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
403 if let Some(state) = lock.tests.get_mut(&test_uuid) {
404 update(&mut state.test);
405 }
406 }
407
408 pub fn set_test_case_id(&self, test_case_id: impl Into<String>) {
410 let test_case_id = test_case_id.into();
411 self.update_test_case(|test| test.test_case_id = Some(test_case_id));
412 }
413
414 pub fn set_history_id(&self, history_id: impl Into<String>) {
416 let history_id = history_id.into();
417 self.update_test_case(|test| test.history_id = Some(history_id));
418 }
419
420 pub fn add_label(&self, name: impl Into<String>, value: impl Into<String>) {
422 let name = name.into();
423 let value = value.into();
424 self.update_test_case(|test| {
425 if matches!(name.as_str(), "parentSuite" | "suite" | "subSuite") {
426 test.labels.retain(|label| label.name != name);
427 }
428 test.labels.push(Label {
429 name: name.clone(),
430 value: value.clone(),
431 });
432 });
433 }
434
435 pub fn add_link(
437 &self,
438 url: impl Into<String>,
439 name: Option<String>,
440 link_type: Option<String>,
441 ) {
442 let url = url.into();
443 self.update_test_case(|test| {
444 test.links.push(Link {
445 name,
446 url,
447 link_type,
448 })
449 });
450 }
451
452 pub fn add_parameter(&self, name: impl Into<String>, value: impl Into<String>) {
454 self.add_parameter_with_options(name, value, None, None);
455 }
456
457 pub fn add_parameter_with_options(
459 &self,
460 name: impl Into<String>,
461 value: impl Into<String>,
462 excluded: Option<bool>,
463 mode: Option<ParameterMode>,
464 ) {
465 let name = name.into();
466 let value = value.into();
467 self.update_test_case(|test| {
468 test.parameters.push(Parameter {
469 name,
470 value,
471 excluded,
472 mode,
473 })
474 });
475 }
476
477 pub fn start_scope(&self, name: Option<String>) -> String {
479 let uuid = next_id();
480 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
481 lock.scopes.insert(
482 uuid.clone(),
483 ScopeState {
484 container: TestResultContainer {
485 uuid: uuid.clone(),
486 name,
487 start: Some(now_millis()),
488 ..Default::default()
489 },
490 running_fixture: None,
491 },
492 );
493 uuid
494 }
495
496 pub fn link_scope_to_test(&self, scope_uuid: &str, test_uuid: &str) {
498 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
499 let has_scope = lock.scopes.contains_key(scope_uuid);
500 let has_test = lock.tests.contains_key(test_uuid);
501 if !(has_scope && has_test) {
502 return;
503 }
504
505 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
506 if !scope
507 .container
508 .children
509 .iter()
510 .any(|child| child == test_uuid)
511 {
512 scope.container.children.push(test_uuid.to_string());
513 }
514 }
515 if let Some(test) = lock.tests.get_mut(test_uuid) {
516 if !test.linked_scopes.iter().any(|scope| scope == scope_uuid) {
517 test.linked_scopes.push(scope_uuid.to_string());
518 }
519 }
520 }
521
522 pub fn stop_scope(&self, scope_uuid: &str) {
524 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
525 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
526 finish_running_fixture(scope);
527 normalize_container_times(&mut scope.container, now_millis());
528 }
529 ACTIVE_SCOPE_ROOT.with(|cell| {
530 if cell.borrow().as_deref() == Some(scope_uuid) {
531 *cell.borrow_mut() = None;
532 }
533 });
534 }
535
536 pub fn write_scope(&self, scope_uuid: &str) {
538 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
539 if let Some(scope) = lock.scopes.remove(scope_uuid) {
540 let _ = self.writer.write_container(&scope.container);
541 }
542 }
543
544 pub fn start_before_fixture(&self, scope_uuid: &str, name: impl Into<String>) {
546 self.start_fixture(scope_uuid, name.into(), FixtureKind::Before);
547 }
548
549 pub fn stop_before_fixture(
551 &self,
552 scope_uuid: &str,
553 status: Status,
554 details: Option<StatusDetails>,
555 ) {
556 self.stop_fixture(scope_uuid, FixtureKind::Before, status, details);
557 }
558
559 pub fn start_after_fixture(&self, scope_uuid: &str, name: impl Into<String>) {
561 self.start_fixture(scope_uuid, name.into(), FixtureKind::After);
562 }
563
564 pub fn stop_after_fixture(
566 &self,
567 scope_uuid: &str,
568 status: Status,
569 details: Option<StatusDetails>,
570 ) {
571 self.stop_fixture(scope_uuid, FixtureKind::After, status, details);
572 }
573
574 pub fn add_attachment(
576 &self,
577 name: impl Into<String>,
578 content_type: impl Into<String>,
579 bytes: &[u8],
580 ) {
581 let name = name.into();
582 let content_type = content_type.into();
583 let id = next_id();
584 if let Ok((source, _)) =
585 self.writer
586 .write_attachment_auto(&id, Some(&name), Some(&content_type), bytes)
587 {
588 let attachment = Attachment {
589 name,
590 source,
591 content_type,
592 };
593 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
594 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
595 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
596 if let Some(fixture) = scope.running_fixture.as_mut() {
597 if let Some(step) = fixture.step_stack.last_mut() {
598 step.attachments.push(attachment);
599 } else {
600 fixture.fixture.attachments.push(attachment);
601 }
602 return;
603 }
604 }
605 }
606
607 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) {
608 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
609 if let Some(step) = test_state.step_stack.last_mut() {
610 step.attachments.push(attachment);
611 } else {
612 test_state.test.attachments.push(attachment);
613 }
614 }
615 }
616 }
617 }
618
619 pub fn add_http_exchange(&self, exchange: HttpExchange) {
621 self.add_http_exchange_named(HTTP_EXCHANGE_ATTACHMENT_NAME, exchange);
622 }
623
624 pub fn add_http_exchange_named(&self, name: impl Into<String>, exchange: HttpExchange) {
626 if let Ok(bytes) = serde_json::to_vec(&exchange) {
627 self.add_attachment(name, HTTP_EXCHANGE_ATTACHMENT_MIME, &bytes);
628 }
629 }
630
631 pub fn add_global_attachment(
633 &self,
634 name: impl Into<String>,
635 content_type: impl Into<String>,
636 bytes: &[u8],
637 ) -> std::io::Result<()> {
638 let name = name.into();
639 let content_type = content_type.into();
640 let (source, _) = self.writer.write_attachment_auto(
641 &next_id(),
642 Some(&name),
643 Some(&content_type),
644 bytes,
645 )?;
646 self.writer
647 .write_globals_typed(&Globals {
648 attachments: vec![GlobalAttachment {
649 name,
650 source,
651 content_type,
652 }],
653 errors: Vec::new(),
654 })
655 .map(|_| ())
656 }
657
658 pub fn add_global_error(
660 &self,
661 message: impl Into<String>,
662 trace: Option<String>,
663 ) -> std::io::Result<()> {
664 self.writer
665 .write_globals_typed(&Globals {
666 attachments: Vec::new(),
667 errors: vec![GlobalError {
668 message: message.into(),
669 trace,
670 }],
671 })
672 .map(|_| ())
673 }
674
675 pub fn start_step(&self, name: impl Into<String>) {
677 self.start_step_at(name, None);
678 }
679
680 pub fn start_step_at(&self, name: impl Into<String>, timestamp: Option<i64>) -> i64 {
682 let timestamp = timestamp.unwrap_or_else(now_millis);
683 let step = RunningStep::new(name, timestamp, RunningStepKind::Step);
684 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
685
686 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
687 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
688 if let Some(fixture) = scope.running_fixture.as_mut() {
689 fixture.step_stack.push(step);
690 return timestamp;
691 }
692 }
693 }
694
695 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) {
696 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
697 test_state.step_stack.push(step);
698 }
699 }
700
701 timestamp
702 }
703
704 pub fn start_stage(&self, name: impl Into<String>) {
706 self.start_stage_at(name, None);
707 }
708
709 pub fn start_stage_at(&self, name: impl Into<String>, timestamp: Option<i64>) -> i64 {
711 let timestamp = timestamp.unwrap_or_else(now_millis);
712 let name = name.into();
713 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
714
715 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
716 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
717 if let Some(fixture) = scope.running_fixture.as_mut() {
718 start_stage_in_stack(
719 &mut fixture.step_stack,
720 &mut fixture.fixture.steps,
721 name,
722 timestamp,
723 );
724 return timestamp;
725 }
726 }
727 }
728
729 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) {
730 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
731 start_stage_in_stack(
732 &mut test_state.step_stack,
733 &mut test_state.test.steps,
734 name,
735 timestamp,
736 );
737 }
738 }
739
740 timestamp
741 }
742
743 pub fn stop_step(&self, status: Status, details: Option<StatusDetails>) {
745 self.stop_step_at(None, status, details);
746 }
747
748 pub fn stop_step_at(
750 &self,
751 timestamp: Option<i64>,
752 status: Status,
753 details: Option<StatusDetails>,
754 ) {
755 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
756
757 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
758 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
759 if let Some(fixture) = scope.running_fixture.as_mut() {
760 stop_one_step(
761 &mut fixture.step_stack,
762 &mut fixture.fixture.steps,
763 timestamp,
764 status,
765 details,
766 );
767 return;
768 }
769 }
770 }
771
772 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) {
773 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
774 stop_one_step(
775 &mut test_state.step_stack,
776 &mut test_state.test.steps,
777 timestamp,
778 status,
779 details,
780 );
781 }
782 }
783 }
784
785 pub fn set_current_step_display_name(&self, name: impl Into<String>) {
787 let name = name.into();
788 self.update_current_step(
789 move |step| step.name = name,
790 "attempted to rename current step, but no step is active",
791 );
792 }
793
794 pub fn add_current_step_parameter(&self, name: impl Into<String>, value: impl Into<String>) {
796 self.add_current_step_parameter_with_options(name, value, None, None);
797 }
798
799 pub fn add_current_step_parameter_with_options(
801 &self,
802 name: impl Into<String>,
803 value: impl Into<String>,
804 excluded: Option<bool>,
805 mode: Option<ParameterMode>,
806 ) {
807 let parameter = Parameter {
808 name: name.into(),
809 value: value.into(),
810 excluded,
811 mode,
812 };
813 self.update_current_step(
814 move |step| step.parameters.push(parameter),
815 "attempted to add a parameter to the current step, but no step is active",
816 );
817 }
818
819 fn start_fixture(&self, scope_uuid: &str, name: String, kind: FixtureKind) {
820 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
821 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
822 finish_running_fixture(scope);
823 scope.running_fixture = Some(RunningFixture {
824 kind,
825 fixture: FixtureResult {
826 name,
827 stage: Some(Stage::Running),
828 start: Some(now_millis()),
829 ..Default::default()
830 },
831 step_stack: Vec::new(),
832 });
833 ACTIVE_SCOPE_ROOT.with(|cell| *cell.borrow_mut() = Some(scope_uuid.to_string()));
834 }
835 }
836
837 pub(crate) fn update_current_step<F>(&self, update: F, missing_step_message: &str)
838 where
839 F: FnOnce(&mut StepResult),
840 {
841 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
842
843 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
844 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
845 if let Some(fixture) = scope.running_fixture.as_mut() {
846 if let Some(step) = fixture.step_stack.last_mut() {
847 update(step);
848 return;
849 }
850 }
851 }
852 }
853
854 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().last().cloned()) {
855 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
856 if let Some(step) = test_state.step_stack.last_mut() {
857 update(step);
858 return;
859 }
860 }
861 }
862
863 eprintln!("[allure-rust] {missing_step_message}");
864 }
865
866 fn stop_fixture(
867 &self,
868 scope_uuid: &str,
869 expected_kind: FixtureKind,
870 status: Status,
871 details: Option<StatusDetails>,
872 ) {
873 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
874 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
875 if let Some(mut fixture) = scope.running_fixture.take() {
876 if !matches!(
877 (&fixture.kind, &expected_kind),
878 (FixtureKind::Before, FixtureKind::Before)
879 | (FixtureKind::After, FixtureKind::After)
880 ) {
881 scope.running_fixture = Some(fixture);
882 return;
883 }
884
885 finalize_steps(
886 &mut fixture.step_stack,
887 &mut fixture.fixture.steps,
888 status.clone(),
889 details.clone(),
890 );
891 fixture.fixture.status = Some(status);
892 fixture.fixture.status_details = details;
893 fixture.fixture.stage = Some(Stage::Finished);
894 normalize_fixture_result(&mut fixture.fixture, now_millis());
895 match fixture.kind {
896 FixtureKind::Before => scope.container.befores.push(fixture.fixture),
897 FixtureKind::After => scope.container.afters.push(fixture.fixture),
898 }
899 }
900 }
901 ACTIVE_SCOPE_ROOT.with(|cell| {
902 if cell.borrow().as_deref() == Some(scope_uuid) {
903 *cell.borrow_mut() = None;
904 }
905 });
906 }
907}
908
909fn stop_one_step(
910 stack: &mut Vec<RunningStep>,
911 root_steps: &mut Vec<StepResult>,
912 timestamp: Option<i64>,
913 status: Status,
914 details: Option<StatusDetails>,
915) {
916 close_active_stages(
917 stack,
918 root_steps,
919 timestamp,
920 status.clone(),
921 details.clone(),
922 );
923 if stack.is_empty() {
924 return;
925 }
926 stop_top_step(stack, root_steps, timestamp, status, details);
927}
928
929fn start_stage_in_stack(
930 stack: &mut Vec<RunningStep>,
931 root_steps: &mut Vec<StepResult>,
932 name: impl Into<String>,
933 timestamp: i64,
934) {
935 if matches!(
936 stack.last().map(|step| step.kind),
937 Some(RunningStepKind::Stage)
938 ) {
939 stop_top_step(stack, root_steps, Some(timestamp), Status::Passed, None);
940 }
941 stack.push(RunningStep::new(name, timestamp, RunningStepKind::Stage));
942}
943
944fn close_active_stages(
945 stack: &mut Vec<RunningStep>,
946 root_steps: &mut Vec<StepResult>,
947 timestamp: Option<i64>,
948 status: Status,
949 details: Option<StatusDetails>,
950) {
951 while matches!(
952 stack.last().map(|step| step.kind),
953 Some(RunningStepKind::Stage)
954 ) {
955 stop_top_step(
956 stack,
957 root_steps,
958 timestamp,
959 status.clone(),
960 details.clone(),
961 );
962 }
963}
964
965fn stop_top_step(
966 stack: &mut Vec<RunningStep>,
967 root_steps: &mut Vec<StepResult>,
968 timestamp: Option<i64>,
969 status: Status,
970 details: Option<StatusDetails>,
971) {
972 if let Some(mut step) = stack.pop() {
973 finish_step_result(&mut step.result, timestamp, status, details);
974 if let Some(parent) = stack.last_mut() {
975 parent.steps.push(step.result);
976 } else {
977 root_steps.push(step.result);
978 }
979 }
980}
981
982fn finish_step_result(
983 step: &mut StepResult,
984 timestamp: Option<i64>,
985 status: Status,
986 details: Option<StatusDetails>,
987) {
988 step.status.get_or_insert(status);
989 if step.status_details.is_none() {
990 step.status_details = details;
991 }
992 step.stage = Some(Stage::Finished);
993 normalize_step_result(step, timestamp.unwrap_or_else(now_millis));
994 if let Some(stop) = timestamp {
995 step.stop = Some(stop);
996 if step.start.is_none() {
997 step.start = Some(stop);
998 }
999 }
1000}
1001
1002fn finalize_steps(
1003 stack: &mut Vec<RunningStep>,
1004 root_steps: &mut Vec<StepResult>,
1005 context_status: Status,
1006 context_details: Option<StatusDetails>,
1007) {
1008 while let Some(mut step) = stack.pop() {
1009 let status = match step.kind {
1010 RunningStepKind::Step => Status::Broken,
1011 RunningStepKind::Stage => context_status.clone(),
1012 };
1013 let details = match step.kind {
1014 RunningStepKind::Step => None,
1015 RunningStepKind::Stage => context_details.clone(),
1016 };
1017 finish_step_result(&mut step.result, None, status, details);
1018 if let Some(parent) = stack.last_mut() {
1019 parent.steps.push(step.result);
1020 } else {
1021 root_steps.push(step.result);
1022 }
1023 }
1024}
1025
1026fn finish_running_fixture(scope: &mut ScopeState) {
1027 if let Some(mut fixture) = scope.running_fixture.take() {
1028 finalize_steps(
1029 &mut fixture.step_stack,
1030 &mut fixture.fixture.steps,
1031 Status::Broken,
1032 None,
1033 );
1034 fixture.fixture.status.get_or_insert(Status::Broken);
1035 fixture.fixture.stage = Some(Stage::Finished);
1036 normalize_fixture_result(&mut fixture.fixture, now_millis());
1037 match fixture.kind {
1038 FixtureKind::Before => scope.container.befores.push(fixture.fixture),
1039 FixtureKind::After => scope.container.afters.push(fixture.fixture),
1040 }
1041 }
1042}
1043
1044fn merge_before_scope_metadata(
1045 lock: &LifecycleState,
1046 test: &mut TestResult,
1047 linked_scopes: &[String],
1048) {
1049 for scope_uuid in linked_scopes {
1050 if let Some(scope) = lock.scopes.get(scope_uuid) {
1051 for link in &scope.container.links {
1052 test.links.push(link.clone());
1053 }
1054 for fixture in &scope.container.befores {
1055 for parameter in &fixture.parameters {
1056 test.parameters.push(parameter.clone());
1057 }
1058 }
1059 }
1060 }
1061}
1062
1063#[cfg(test)]
1064#[path = "lifecycle_tests.rs"]
1065mod lifecycle_tests;