1use std::{
2 cell::RefCell,
3 cmp,
4 collections::HashMap,
5 sync::{
6 atomic::{AtomicU64, Ordering},
7 Arc, Mutex,
8 },
9 time::{SystemTime, UNIX_EPOCH},
10};
11
12use crate::{
13 md5::md5_hex,
14 model::{
15 Attachment, FixtureResult, Label, Link, Parameter, Stage, Status, StatusDetails,
16 StepResult, TestResult, TestResultContainer,
17 },
18 writer::FileSystemResultsWriter,
19};
20
21thread_local! {
22 static ACTIVE_TEST_ROOT: RefCell<Option<String>> = const { RefCell::new(None) };
23 static ACTIVE_SCOPE_ROOT: RefCell<Option<String>> = const { RefCell::new(None) };
24}
25
26static ID_COUNTER: AtomicU64 = AtomicU64::new(1);
27
28fn now_millis() -> i64 {
29 SystemTime::now()
30 .duration_since(UNIX_EPOCH)
31 .map(|d| d.as_millis() as i64)
32 .unwrap_or_default()
33}
34
35fn next_id() -> String {
36 format!(
37 "{}-{}",
38 now_millis(),
39 ID_COUNTER.fetch_add(1, Ordering::Relaxed)
40 )
41}
42
43fn round_millis(value: f64) -> i64 {
44 value.round() as i64
45}
46
47fn normalize_times(
48 start: Option<i64>,
49 stop: Option<i64>,
50 duration: Option<f64>,
51 fallback_stop: i64,
52) -> (Option<i64>, Option<i64>) {
53 let rounded_duration = duration.map(round_millis).map(|value| cmp::max(value, 0));
54
55 let (start, stop) = match (start, stop, rounded_duration) {
56 (Some(start), Some(stop), _) => (start, cmp::max(stop, start)),
57 (Some(start), None, Some(duration)) => (start, start.saturating_add(duration)),
58 (None, Some(stop), Some(duration)) => (stop.saturating_sub(duration), stop),
59 (Some(start), None, None) => (start, cmp::max(fallback_stop, start)),
60 (None, Some(stop), None) => (stop, stop),
61 (None, None, Some(duration)) => {
62 let stop = fallback_stop;
63 (stop.saturating_sub(duration), stop)
64 }
65 (None, None, None) => (fallback_stop, fallback_stop),
66 };
67
68 (Some(start), Some(stop))
69}
70
71fn normalize_step_result(step: &mut StepResult, fallback_stop: i64) {
72 (step.start, step.stop) = normalize_times(step.start, step.stop, None, fallback_stop);
73 for nested in &mut step.steps {
74 normalize_step_result(nested, step.stop.unwrap_or(fallback_stop));
75 }
76}
77
78fn normalize_fixture_result(fixture: &mut FixtureResult, fallback_stop: i64) {
79 (fixture.start, fixture.stop) =
80 normalize_times(fixture.start, fixture.stop, None, fallback_stop);
81 let fixture_stop = fixture.stop.unwrap_or(fallback_stop);
82 for step in &mut fixture.steps {
83 normalize_step_result(step, fixture_stop);
84 }
85}
86
87fn normalize_test_result(test: &mut TestResult, fallback_stop: i64) {
88 (test.start, test.stop) = normalize_times(test.start, test.stop, None, fallback_stop);
89 let test_stop = test.stop.unwrap_or(fallback_stop);
90 for step in &mut test.steps {
91 normalize_step_result(step, test_stop);
92 }
93}
94
95fn normalize_container_times(container: &mut TestResultContainer, fallback_stop: i64) {
96 (container.start, container.stop) =
97 normalize_times(container.start, container.stop, None, fallback_stop);
98 let container_stop = container.stop.unwrap_or(fallback_stop);
99 for fixture in &mut container.befores {
100 normalize_fixture_result(fixture, container_stop);
101 }
102 for fixture in &mut container.afters {
103 normalize_fixture_result(fixture, container_stop);
104 }
105}
106
107fn derive_test_case_id(test: &TestResult) -> Option<String> {
108 test.test_case_id
109 .clone()
110 .or_else(|| test.full_name.clone().map(|full_name| md5_hex(&full_name)))
111}
112
113fn derive_history_id(test: &TestResult) -> Option<String> {
114 let base = test
115 .test_case_id
116 .as_ref()
117 .or(test.full_name.as_ref())
118 .or(Some(&test.name))?;
119
120 let mut parameters = test
121 .parameters
122 .iter()
123 .filter(|parameter| parameter.excluded != Some(true))
124 .map(|parameter| format!("{}:{}", parameter.name, parameter.value))
125 .collect::<Vec<_>>();
126 parameters.sort();
127 let parameter_hash = md5_hex(¶meters.join(","));
128
129 Some(md5_hex(&format!("{base}:{parameter_hash}")))
130}
131
132#[derive(Clone)]
133pub struct AllureRuntime {
134 writer: Arc<FileSystemResultsWriter>,
135}
136
137impl AllureRuntime {
138 pub fn new(writer: FileSystemResultsWriter) -> Self {
139 Self {
140 writer: Arc::new(writer),
141 }
142 }
143
144 pub fn lifecycle(&self) -> AllureLifecycle {
145 AllureLifecycle {
146 writer: Arc::clone(&self.writer),
147 state: Arc::new(Mutex::new(LifecycleState::default())),
148 }
149 }
150}
151
152#[derive(Clone)]
153pub struct AllureLifecycle {
154 writer: Arc<FileSystemResultsWriter>,
155 state: Arc<Mutex<LifecycleState>>,
156}
157
158#[derive(Debug, Clone, Default)]
159pub struct StartTestCaseParams {
160 pub uuid: Option<String>,
161 pub name: String,
162 pub full_name: Option<String>,
163 pub history_id: Option<String>,
164 pub test_case_id: Option<String>,
165 pub description: Option<String>,
166 pub description_html: Option<String>,
167 pub status: Option<Status>,
168 pub status_details: Option<StatusDetails>,
169 pub stage: Option<Stage>,
170 pub labels: Vec<Label>,
171 pub links: Vec<Link>,
172 pub parameters: Vec<Parameter>,
173 pub steps: Vec<StepResult>,
174 pub attachments: Vec<Attachment>,
175 pub title_path: Option<Vec<String>>,
176 pub start: Option<i64>,
177 pub stop: Option<i64>,
178}
179
180impl StartTestCaseParams {
181 pub fn new(name: impl Into<String>) -> Self {
182 Self {
183 name: name.into(),
184 ..Default::default()
185 }
186 }
187
188 pub fn with_full_name(mut self, full_name: impl Into<String>) -> Self {
189 self.full_name = Some(full_name.into());
190 self
191 }
192}
193
194impl From<String> for StartTestCaseParams {
195 fn from(name: String) -> Self {
196 Self {
197 name,
198 ..Default::default()
199 }
200 }
201}
202
203impl From<&str> for StartTestCaseParams {
204 fn from(name: &str) -> Self {
205 Self::from(name.to_string())
206 }
207}
208
209#[derive(Default)]
210struct LifecycleState {
211 tests: HashMap<String, TestState>,
212 scopes: HashMap<String, ScopeState>,
213}
214
215struct TestState {
216 test: TestResult,
217 step_stack: Vec<StepResult>,
218 linked_scopes: Vec<String>,
219}
220
221struct ScopeState {
222 container: TestResultContainer,
223 running_fixture: Option<RunningFixture>,
224}
225
226struct RunningFixture {
227 kind: FixtureKind,
228 fixture: FixtureResult,
229 step_stack: Vec<StepResult>,
230}
231
232enum FixtureKind {
233 Before,
234 After,
235}
236
237impl AllureLifecycle {
238 pub fn start_test_case(&self, params: impl Into<StartTestCaseParams>) {
239 let params = params.into();
240 let name = params.name;
241 let uuid = params.uuid.unwrap_or_else(next_id);
242 let full_name = params.full_name.or_else(|| Some(name.clone()));
243
244 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
245 lock.tests.insert(
246 uuid.clone(),
247 TestState {
248 test: TestResult {
249 uuid: uuid.clone(),
250 name,
251 full_name,
252 history_id: params.history_id,
253 test_case_id: params.test_case_id,
254 description: params.description,
255 description_html: params.description_html,
256 status: params.status,
257 status_details: params.status_details,
258 stage: params.stage.or(Some(Stage::Running)),
259 labels: params.labels,
260 links: params.links,
261 parameters: params.parameters,
262 steps: params.steps,
263 attachments: params.attachments,
264 title_path: params.title_path,
265 start: params.start.or_else(|| Some(now_millis())),
266 stop: params.stop,
267 },
268 step_stack: Vec::new(),
269 linked_scopes: Vec::new(),
270 },
271 );
272 ACTIVE_TEST_ROOT.with(|cell| *cell.borrow_mut() = Some(uuid));
273 }
274
275 pub fn current_test_uuid(&self) -> Option<String> {
276 ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone())
277 }
278
279 pub fn stop_test_case(&self, status: Status, details: Option<StatusDetails>) {
280 let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone()) else {
281 return;
282 };
283
284 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
285 if let Some(mut state) = lock.tests.remove(&test_uuid) {
286 finalize_steps(&mut state.step_stack, &mut state.test.steps);
287 merge_before_scope_metadata(&lock, &mut state.test, &state.linked_scopes);
288
289 state.test.status = Some(status);
290 state.test.status_details = details;
291 state.test.stage = Some(Stage::Finished);
292 let fallback_stop = now_millis();
293 state.test.test_case_id = derive_test_case_id(&state.test);
294 state.test.history_id = derive_history_id(&state.test);
295 normalize_test_result(&mut state.test, fallback_stop);
296 let _ = self.writer.write_result(&state.test);
297 }
298
299 ACTIVE_TEST_ROOT.with(|cell| {
300 if cell.borrow().as_deref() == Some(test_uuid.as_str()) {
301 *cell.borrow_mut() = None;
302 }
303 });
304 }
305
306 pub fn update_test_case<F>(&self, update: F)
307 where
308 F: FnOnce(&mut TestResult),
309 {
310 let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone()) else {
311 return;
312 };
313 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
314 if let Some(state) = lock.tests.get_mut(&test_uuid) {
315 update(&mut state.test);
316 }
317 }
318
319 pub fn set_test_case_id(&self, test_case_id: impl Into<String>) {
320 let test_case_id = test_case_id.into();
321 self.update_test_case(|test| test.test_case_id = Some(test_case_id));
322 }
323
324 pub fn add_label(&self, name: impl Into<String>, value: impl Into<String>) {
325 let name = name.into();
326 let value = value.into();
327 self.update_test_case(|test| {
328 if matches!(name.as_str(), "parentSuite" | "suite" | "subSuite") {
329 test.labels.retain(|label| label.name != name);
330 }
331 test.labels.push(Label {
332 name: name.clone(),
333 value: value.clone(),
334 });
335 });
336 }
337
338 pub fn add_link(
339 &self,
340 url: impl Into<String>,
341 name: Option<String>,
342 link_type: Option<String>,
343 ) {
344 let url = url.into();
345 self.update_test_case(|test| {
346 test.links.push(Link {
347 name,
348 url,
349 link_type,
350 })
351 });
352 }
353
354 pub fn add_parameter(&self, name: impl Into<String>, value: impl Into<String>) {
355 let name = name.into();
356 let value = value.into();
357 self.update_test_case(|test| {
358 test.parameters.push(Parameter {
359 name,
360 value,
361 excluded: None,
362 mode: None,
363 })
364 });
365 }
366
367 pub fn start_scope(&self, name: Option<String>) -> String {
368 let uuid = next_id();
369 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
370 lock.scopes.insert(
371 uuid.clone(),
372 ScopeState {
373 container: TestResultContainer {
374 uuid: uuid.clone(),
375 name,
376 start: Some(now_millis()),
377 ..Default::default()
378 },
379 running_fixture: None,
380 },
381 );
382 uuid
383 }
384
385 pub fn link_scope_to_test(&self, scope_uuid: &str, test_uuid: &str) {
386 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
387 let has_scope = lock.scopes.contains_key(scope_uuid);
388 let has_test = lock.tests.contains_key(test_uuid);
389 if !(has_scope && has_test) {
390 return;
391 }
392
393 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
394 if !scope
395 .container
396 .children
397 .iter()
398 .any(|child| child == test_uuid)
399 {
400 scope.container.children.push(test_uuid.to_string());
401 }
402 }
403 if let Some(test) = lock.tests.get_mut(test_uuid) {
404 if !test.linked_scopes.iter().any(|scope| scope == scope_uuid) {
405 test.linked_scopes.push(scope_uuid.to_string());
406 }
407 }
408 }
409
410 pub fn stop_scope(&self, scope_uuid: &str) {
411 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
412 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
413 finish_running_fixture(scope);
414 normalize_container_times(&mut scope.container, now_millis());
415 }
416 ACTIVE_SCOPE_ROOT.with(|cell| {
417 if cell.borrow().as_deref() == Some(scope_uuid) {
418 *cell.borrow_mut() = None;
419 }
420 });
421 }
422
423 pub fn write_scope(&self, scope_uuid: &str) {
424 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
425 if let Some(scope) = lock.scopes.remove(scope_uuid) {
426 let _ = self.writer.write_container(&scope.container);
427 }
428 }
429
430 pub fn start_before_fixture(&self, scope_uuid: &str, name: impl Into<String>) {
431 self.start_fixture(scope_uuid, name.into(), FixtureKind::Before);
432 }
433
434 pub fn stop_before_fixture(
435 &self,
436 scope_uuid: &str,
437 status: Status,
438 details: Option<StatusDetails>,
439 ) {
440 self.stop_fixture(scope_uuid, FixtureKind::Before, status, details);
441 }
442
443 pub fn start_after_fixture(&self, scope_uuid: &str, name: impl Into<String>) {
444 self.start_fixture(scope_uuid, name.into(), FixtureKind::After);
445 }
446
447 pub fn stop_after_fixture(
448 &self,
449 scope_uuid: &str,
450 status: Status,
451 details: Option<StatusDetails>,
452 ) {
453 self.stop_fixture(scope_uuid, FixtureKind::After, status, details);
454 }
455
456 pub fn add_attachment(
457 &self,
458 name: impl Into<String>,
459 content_type: impl Into<String>,
460 bytes: &[u8],
461 ) {
462 let name = name.into();
463 let content_type = content_type.into();
464 let id = next_id();
465 if let Ok((source, _)) =
466 self.writer
467 .write_attachment_auto(&id, Some(&name), Some(&content_type), bytes)
468 {
469 let attachment = Attachment {
470 name,
471 source,
472 content_type,
473 };
474 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
475 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
476 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
477 if let Some(fixture) = scope.running_fixture.as_mut() {
478 if let Some(step) = fixture.step_stack.last_mut() {
479 step.attachments.push(attachment);
480 } else {
481 fixture.fixture.attachments.push(attachment);
482 }
483 return;
484 }
485 }
486 }
487
488 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone()) {
489 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
490 if let Some(step) = test_state.step_stack.last_mut() {
491 step.attachments.push(attachment);
492 } else {
493 test_state.test.attachments.push(attachment);
494 }
495 }
496 }
497 }
498 }
499
500 pub fn start_step(&self, name: impl Into<String>) {
501 self.start_step_at(name, None);
502 }
503
504 pub fn start_step_at(&self, name: impl Into<String>, timestamp: Option<i64>) -> i64 {
505 let timestamp = timestamp.unwrap_or_else(now_millis);
506 let step = StepResult {
507 name: name.into(),
508 stage: Some(Stage::Running),
509 start: Some(timestamp),
510 ..Default::default()
511 };
512 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
513
514 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
515 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
516 if let Some(fixture) = scope.running_fixture.as_mut() {
517 fixture.step_stack.push(step);
518 return timestamp;
519 }
520 }
521 }
522
523 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone()) {
524 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
525 test_state.step_stack.push(step);
526 }
527 }
528
529 timestamp
530 }
531
532 pub fn stop_step(&self, status: Status, details: Option<StatusDetails>) {
533 self.stop_step_at(None, status, details);
534 }
535
536 pub fn stop_step_at(
537 &self,
538 timestamp: Option<i64>,
539 status: Status,
540 details: Option<StatusDetails>,
541 ) {
542 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
543
544 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
545 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
546 if let Some(fixture) = scope.running_fixture.as_mut() {
547 stop_one_step(
548 &mut fixture.step_stack,
549 &mut fixture.fixture.steps,
550 timestamp,
551 status,
552 details,
553 );
554 return;
555 }
556 }
557 }
558
559 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone()) {
560 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
561 stop_one_step(
562 &mut test_state.step_stack,
563 &mut test_state.test.steps,
564 timestamp,
565 status,
566 details,
567 );
568 }
569 }
570 }
571
572 pub fn set_current_step_display_name(&self, name: impl Into<String>) {
573 let name = name.into();
574 self.update_current_step(
575 move |step| step.name = name,
576 "attempted to rename current step, but no step is active",
577 );
578 }
579
580 pub fn add_current_step_parameter(&self, name: impl Into<String>, value: impl Into<String>) {
581 let parameter = Parameter {
582 name: name.into(),
583 value: value.into(),
584 excluded: None,
585 mode: None,
586 };
587 self.update_current_step(
588 move |step| step.parameters.push(parameter),
589 "attempted to add a parameter to the current step, but no step is active",
590 );
591 }
592
593 fn start_fixture(&self, scope_uuid: &str, name: String, kind: FixtureKind) {
594 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
595 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
596 finish_running_fixture(scope);
597 scope.running_fixture = Some(RunningFixture {
598 kind,
599 fixture: FixtureResult {
600 name,
601 stage: Some(Stage::Running),
602 start: Some(now_millis()),
603 ..Default::default()
604 },
605 step_stack: Vec::new(),
606 });
607 ACTIVE_SCOPE_ROOT.with(|cell| *cell.borrow_mut() = Some(scope_uuid.to_string()));
608 }
609 }
610
611 fn update_current_step<F>(&self, update: F, missing_step_message: &str)
612 where
613 F: FnOnce(&mut StepResult),
614 {
615 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
616
617 if let Some(scope_uuid) = ACTIVE_SCOPE_ROOT.with(|cell| cell.borrow().clone()) {
618 if let Some(scope) = lock.scopes.get_mut(&scope_uuid) {
619 if let Some(fixture) = scope.running_fixture.as_mut() {
620 if let Some(step) = fixture.step_stack.last_mut() {
621 update(step);
622 return;
623 }
624 }
625 }
626 }
627
628 if let Some(test_uuid) = ACTIVE_TEST_ROOT.with(|cell| cell.borrow().clone()) {
629 if let Some(test_state) = lock.tests.get_mut(&test_uuid) {
630 if let Some(step) = test_state.step_stack.last_mut() {
631 update(step);
632 return;
633 }
634 }
635 }
636
637 eprintln!("[allure-rust] {missing_step_message}");
638 }
639
640 fn stop_fixture(
641 &self,
642 scope_uuid: &str,
643 expected_kind: FixtureKind,
644 status: Status,
645 details: Option<StatusDetails>,
646 ) {
647 let mut lock = self.state.lock().expect("poisoned allure lifecycle mutex");
648 if let Some(scope) = lock.scopes.get_mut(scope_uuid) {
649 if let Some(mut fixture) = scope.running_fixture.take() {
650 if !matches!(
651 (&fixture.kind, &expected_kind),
652 (FixtureKind::Before, FixtureKind::Before)
653 | (FixtureKind::After, FixtureKind::After)
654 ) {
655 scope.running_fixture = Some(fixture);
656 return;
657 }
658
659 finalize_steps(&mut fixture.step_stack, &mut fixture.fixture.steps);
660 fixture.fixture.status = Some(status);
661 fixture.fixture.status_details = details;
662 fixture.fixture.stage = Some(Stage::Finished);
663 normalize_fixture_result(&mut fixture.fixture, now_millis());
664 match fixture.kind {
665 FixtureKind::Before => scope.container.befores.push(fixture.fixture),
666 FixtureKind::After => scope.container.afters.push(fixture.fixture),
667 }
668 }
669 }
670 ACTIVE_SCOPE_ROOT.with(|cell| {
671 if cell.borrow().as_deref() == Some(scope_uuid) {
672 *cell.borrow_mut() = None;
673 }
674 });
675 }
676}
677
678fn stop_one_step(
679 stack: &mut Vec<StepResult>,
680 root_steps: &mut Vec<StepResult>,
681 timestamp: Option<i64>,
682 status: Status,
683 details: Option<StatusDetails>,
684) {
685 if let Some(mut step) = stack.pop() {
686 step.status.get_or_insert(status);
687 if step.status_details.is_none() {
688 step.status_details = details;
689 }
690 step.stage = Some(Stage::Finished);
691 normalize_step_result(&mut step, timestamp.unwrap_or_else(now_millis));
692 if let Some(stop) = timestamp {
693 step.stop = Some(stop);
694 if step.start.is_none() {
695 step.start = Some(stop);
696 }
697 }
698 if let Some(parent) = stack.last_mut() {
699 parent.steps.push(step);
700 } else {
701 root_steps.push(step);
702 }
703 }
704}
705
706fn finalize_steps(stack: &mut Vec<StepResult>, root_steps: &mut Vec<StepResult>) {
707 while let Some(mut step) = stack.pop() {
708 step.status.get_or_insert(Status::Broken);
709 step.stage = Some(Stage::Finished);
710 normalize_step_result(&mut step, now_millis());
711 if let Some(parent) = stack.last_mut() {
712 parent.steps.push(step);
713 } else {
714 root_steps.push(step);
715 }
716 }
717}
718
719fn finish_running_fixture(scope: &mut ScopeState) {
720 if let Some(mut fixture) = scope.running_fixture.take() {
721 finalize_steps(&mut fixture.step_stack, &mut fixture.fixture.steps);
722 fixture.fixture.status.get_or_insert(Status::Broken);
723 fixture.fixture.stage = Some(Stage::Finished);
724 normalize_fixture_result(&mut fixture.fixture, now_millis());
725 match fixture.kind {
726 FixtureKind::Before => scope.container.befores.push(fixture.fixture),
727 FixtureKind::After => scope.container.afters.push(fixture.fixture),
728 }
729 }
730}
731
732fn merge_before_scope_metadata(
733 lock: &LifecycleState,
734 test: &mut TestResult,
735 linked_scopes: &[String],
736) {
737 for scope_uuid in linked_scopes {
738 if let Some(scope) = lock.scopes.get(scope_uuid) {
739 for link in &scope.container.links {
740 test.links.push(link.clone());
741 }
742 for fixture in &scope.container.befores {
743 for parameter in &fixture.parameters {
744 test.parameters.push(parameter.clone());
745 }
746 }
747 }
748 }
749}
750
751#[cfg(test)]
752#[path = "lifecycle_tests.rs"]
753mod lifecycle_tests;