Skip to main content

allure_rust_commons/
lifecycle.rs

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(&parameters.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;