Skip to main content

tightbeam/testing/macros/
verification_spec.rs

1//! Verification-spec macros (`tb_assert_spec!`) plus supporting builders.
2//! Provides label helpers, cardinality utilities, and the `BuiltAssertSpec`
3//! wrapper that implements `TBSpec`.
4
5use crate::testing::assertions::{AssertionContract, AssertionLabel, AssertionValue};
6use crate::testing::specs::{SpecViolation, TBSpec};
7use crate::trace::{ConsumedTrace, ExecutionMode};
8
9#[cfg(not(feature = "std"))]
10use alloc::{borrow::Cow, string::String, vec::Vec};
11#[cfg(not(feature = "std"))]
12use core::sync::atomic::{AtomicBool, Ordering};
13#[cfg(feature = "std")]
14use std::borrow::Cow;
15
16#[cfg(feature = "digest")]
17use crate::crypto::hash::{Digest, Sha3_256};
18#[cfg(feature = "policy")]
19use crate::policy::TransitStatus;
20#[cfg(feature = "testing-timing")]
21use crate::testing::schedulability::{SchedulerType, TaskSet};
22#[cfg(feature = "derive")]
23use crate::Errorizable;
24
25// ---------------------------------------------------------------------------
26// Cardinality core
27// ---------------------------------------------------------------------------
28
29#[derive(Copy, Clone, Debug, PartialEq, Eq)]
30pub struct Cardinality {
31	min: u32,
32	max: Option<u32>,
33	must_be_present: bool,
34}
35
36impl Cardinality {
37	pub const fn new(min: u32, max: Option<u32>, must_be_present: bool) -> Self {
38		Self { min, max, must_be_present }
39	}
40	pub const fn exactly(n: u32) -> Self {
41		Self { min: n, max: Some(n), must_be_present: n > 0 }
42	}
43	pub const fn at_least(n: u32) -> Self {
44		Self { min: n, max: None, must_be_present: n > 0 }
45	}
46	pub const fn at_most(n: u32) -> Self {
47		Self { min: 0, max: Some(n), must_be_present: false }
48	}
49	pub const fn between(min: u32, max: u32) -> Self {
50		Self { min, max: Some(max), must_be_present: min > 0 }
51	}
52	pub const fn present() -> Self {
53		Self { min: 1, max: None, must_be_present: true }
54	}
55	pub const fn absent() -> Self {
56		Self { min: 0, max: Some(0), must_be_present: false }
57	}
58	pub fn describe(&self) -> String {
59		match (self.min, self.max) {
60			(0, Some(0)) => "absent".into(),
61			(m, Some(n)) if m == n => format!("exactly {m}"),
62			(m, Some(n)) => format!("between {m} and {n}"),
63			(0, None) => "any".into(),
64			(m, None) => format!("at least {m}"),
65		}
66	}
67	pub fn is_satisfied_by(&self, count: usize) -> bool {
68		let c = count as u32;
69		if c < self.min {
70			return false;
71		}
72		if let Some(mx) = self.max {
73			if c > mx {
74				return false;
75			}
76		}
77		true
78	}
79	pub fn min(&self) -> u32 {
80		self.min
81	}
82	pub fn max(&self) -> Option<u32> {
83		self.max
84	}
85	pub fn must_be_present(&self) -> bool {
86		self.must_be_present
87	}
88}
89
90pub const fn between(min: u32, max: u32) -> Cardinality {
91	Cardinality::between(min, max)
92}
93pub const fn present() -> Cardinality {
94	Cardinality::present()
95}
96pub const fn absent() -> Cardinality {
97	Cardinality::absent()
98}
99
100// ---------------------------------------------------------------------------
101// Spec builder and concrete implementation
102// ---------------------------------------------------------------------------
103
104/// Error type for spec building operations
105#[derive(Debug)]
106#[cfg_attr(feature = "derive", derive(Errorizable))]
107pub enum SpecBuildError {
108	#[cfg_attr(feature = "derive", error("Duplicate label: {0}"))]
109	DuplicateLabel(&'static str),
110	#[cfg_attr(feature = "derive", error("Unknown ordering label: {0}"))]
111	UnknownOrderingLabel(&'static str),
112	#[cfg_attr(feature = "derive", error("Invalid range: {0}"))]
113	InvalidRange(&'static str),
114}
115
116/// Builder for programmatic spec construction
117#[derive(Debug, Clone)]
118pub struct AssertSpecBuilder {
119	id: &'static str,
120	execution_mode: ExecutionMode,
121	gate_decision: Option<TransitStatus>,
122	version_major: u16,
123	version_minor: u16,
124	version_patch: u16,
125	assertions: Vec<(&'static str, Vec<&'static str>, Cardinality, Option<AssertionValue>)>,
126	tag_filter: Option<Vec<&'static str>>,
127	ordering: Vec<&'static str>,
128	#[cfg(feature = "instrument")]
129	required_events: Vec<crate::utils::urn::Urn<'static>>,
130	description: Option<&'static str>,
131	#[cfg(feature = "testing-timing")]
132	schedulability: Option<SchedulabilityAssertion>,
133}
134
135impl AssertSpecBuilder {
136	pub fn new(id: &'static str, execution_mode: ExecutionMode) -> Self {
137		Self {
138			id,
139			execution_mode,
140			gate_decision: None,
141			version_major: 1,
142			version_minor: 0,
143			version_patch: 0,
144			assertions: Vec::new(),
145			tag_filter: None,
146			ordering: Vec::new(),
147			#[cfg(feature = "instrument")]
148			required_events: Vec::new(),
149			description: None,
150			#[cfg(feature = "testing-timing")]
151			schedulability: None,
152		}
153	}
154
155	pub fn version(mut self, maj: u16, min: u16, patch: u16) -> Self {
156		self.version_major = maj;
157		self.version_minor = min;
158		self.version_patch = patch;
159		self
160	}
161
162	pub fn gate_decision(mut self, decision: TransitStatus) -> Self {
163		self.gate_decision = Some(decision);
164		self
165	}
166
167	pub fn tag_filter(mut self, tags: Vec<&'static str>) -> Self {
168		self.tag_filter = Some(tags);
169		self
170	}
171
172	pub fn assertion(
173		mut self,
174		label: &'static str,
175		tags: Vec<&'static str>,
176		cardinality: Cardinality,
177	) -> Result<Self, SpecBuildError> {
178		if self.assertions.iter().any(|(l, _, _, _)| *l == label) {
179			return Err(SpecBuildError::DuplicateLabel(label));
180		}
181		if let Some(mx) = cardinality.max {
182			if mx < cardinality.min {
183				return Err(SpecBuildError::InvalidRange(label));
184			}
185		}
186		self.assertions.push((label, tags, cardinality, None));
187		Ok(self)
188	}
189
190	pub fn assertion_with_value(
191		mut self,
192		label: &'static str,
193		tags: Vec<&'static str>,
194		cardinality: Cardinality,
195		expected_value: Option<AssertionValue>,
196	) -> Result<Self, SpecBuildError> {
197		if self.assertions.iter().any(|(l, _, _, _)| *l == label) {
198			return Err(SpecBuildError::DuplicateLabel(label));
199		}
200		if let Some(mx) = cardinality.max {
201			if mx < cardinality.min {
202				return Err(SpecBuildError::InvalidRange(label));
203			}
204		}
205		self.assertions.push((label, tags, cardinality, expected_value));
206		Ok(self)
207	}
208
209	pub fn ordering(mut self, labels: &[&'static str]) -> Result<Self, SpecBuildError> {
210		for &lbl in labels {
211			if !self.assertions.iter().any(|(l, _, _, _)| *l == lbl) {
212				return Err(SpecBuildError::UnknownOrderingLabel(lbl));
213			}
214			self.ordering.push(lbl);
215		}
216		Ok(self)
217	}
218
219	#[cfg(feature = "instrument")]
220	pub fn required_events(mut self, kinds: &[crate::utils::urn::Urn<'static>]) -> Self {
221		use std::collections::HashSet;
222		let mut seen = HashSet::new();
223		for k in kinds {
224			if seen.insert(k.clone()) {
225				self.required_events.push(k.clone());
226			}
227		}
228		self
229	}
230
231	pub fn description(mut self, desc: &'static str) -> Self {
232		self.description = Some(desc);
233		self
234	}
235
236	#[cfg(feature = "testing-timing")]
237	pub fn schedulability(mut self, assertion: SchedulabilityAssertion) -> Self {
238		self.schedulability = Some(assertion);
239		self
240	}
241
242	pub fn build(self) -> BuiltAssertSpec {
243		BuiltAssertSpec::from_builder(self)
244	}
245}
246
247/// Schedulability assertion for verification
248#[cfg(feature = "testing-timing")]
249#[derive(Debug, Clone)]
250pub struct SchedulabilityAssertion {
251	/// Task set to check
252	pub task_set: TaskSet,
253	/// Whether the task set must be schedulable
254	pub must_be_schedulable: bool,
255}
256
257#[derive(Debug, Clone)]
258pub struct BuiltAssertSpec {
259	inner: AssertSpecBuilder,
260	contracts: Box<[AssertionContract]>,
261	spec_hash: [u8; 32],
262}
263
264impl BuiltAssertSpec {
265	fn from_builder(builder: AssertSpecBuilder) -> Self {
266		let tag_filter = builder.tag_filter.clone();
267		let contracts: Vec<AssertionContract> = builder
268			.assertions
269			.iter()
270			.map(|(label, _tags, card, value)| {
271				let mut contract = if let Some(ref val) = value {
272					AssertionContract::new(AssertionLabel::Custom(Cow::Borrowed(label)), *card).with_value(val.clone())
273				} else {
274					AssertionContract::new(AssertionLabel::Custom(Cow::Borrowed(label)), *card)
275				};
276				if let Some(ref filter) = tag_filter {
277					contract = contract.with_tag_filter(filter.clone());
278				}
279				contract
280			})
281			.collect();
282		let spec_hash = Self::compute_hash(
283			builder.id,
284			builder.execution_mode,
285			builder.gate_decision,
286			builder.version_major,
287			builder.version_minor,
288			builder.version_patch,
289			&contracts,
290			builder.tag_filter.as_deref(),
291			#[cfg(feature = "instrument")]
292			&builder.required_events,
293			#[cfg(feature = "testing-timing")]
294			builder.schedulability.as_ref(),
295		);
296		Self { inner: builder, contracts: contracts.into_boxed_slice(), spec_hash }
297	}
298
299	#[allow(clippy::too_many_arguments)]
300	fn compute_hash(
301		id: &'static str,
302		mode: ExecutionMode,
303		gate: Option<TransitStatus>,
304		version_major: u16,
305		version_minor: u16,
306		version_patch: u16,
307		contracts: &[AssertionContract],
308		tag_filter: Option<&[&'static str]>,
309		#[cfg(feature = "instrument")] events: &[crate::utils::urn::Urn<'static>],
310		#[cfg(feature = "testing-timing")] schedulability: Option<&SchedulabilityAssertion>,
311	) -> [u8; 32] {
312		let mut h = Sha3_256::new();
313		// Domain tag + version triple
314		h.update(b"TBSP");
315		h.update(version_major.to_be_bytes());
316		h.update(version_minor.to_be_bytes());
317		h.update(version_patch.to_be_bytes());
318		h.update(id.as_bytes());
319		let mode_code = match mode {
320			ExecutionMode::Accept => 0u8,
321			ExecutionMode::Reject => 1u8,
322			ExecutionMode::Error => 2u8,
323		};
324		h.update([mode_code]);
325		match gate {
326			Some(g) => {
327				h.update([1u8]);
328				h.update([g as u8]);
329			}
330			None => h.update([0u8]),
331		}
332		// Include tag_filter in hash if present
333		if let Some(tags) = tag_filter {
334			h.update([1u8]);
335			h.update((tags.len() as u32).to_be_bytes());
336			for tag in tags {
337				h.update(tag.as_bytes());
338			}
339		} else {
340			h.update([0u8]);
341		}
342		// Normalize assertion order independent of insertion sequence
343		let mut norm: Vec<(&str, u32, Option<u32>, bool)> = Vec::with_capacity(contracts.len());
344		for c in contracts {
345			let AssertionLabel::Custom(lbl) = &c.label;
346			norm.push((
347				lbl.as_ref(),
348				c.cardinality.min,
349				c.cardinality.max,
350				c.cardinality.must_be_present,
351			));
352		}
353		norm.sort_by(|a, b| a.0.cmp(b.0)); // label only
354		for (lbl, min, max, must) in norm {
355			h.update(lbl.as_bytes());
356			h.update(min.to_be_bytes());
357			match max {
358				Some(m) => {
359					h.update([1u8]);
360					h.update(m.to_be_bytes());
361				}
362				None => h.update([0u8]),
363			}
364			h.update([must as u8]);
365		}
366		#[cfg(feature = "instrument")]
367		{
368			// Sort events for deterministic hashing (by string representation)
369			let mut sorted_events: Vec<String> = events.iter().map(|e| e.to_string()).collect();
370			sorted_events.sort();
371			for urn_str in sorted_events {
372				h.update(urn_str.as_bytes());
373			}
374		}
375		#[cfg(feature = "testing-timing")]
376		{
377			if let Some(schedule) = schedulability {
378				h.update([1u8]); // Has schedulability
379				h.update([schedule.task_set.scheduler as u8]);
380				h.update([schedule.must_be_schedulable as u8]);
381				h.update((schedule.task_set.tasks.len() as u32).to_be_bytes());
382				// Hash task set: sort tasks by ID for deterministic hashing
383				let mut tasks: Vec<_> = schedule.task_set.tasks.iter().collect();
384				tasks.sort_by(|a, b| a.id.cmp(&b.id));
385				for task in tasks {
386					h.update(task.id.as_bytes());
387					h.update(task.period.as_nanos().to_be_bytes());
388					h.update(task.deadline.as_nanos().to_be_bytes());
389					h.update(task.wcet.as_nanos().to_be_bytes());
390				}
391			} else {
392				h.update([0u8]); // No schedulability
393			}
394		}
395		let out = h.finalize();
396		let mut arr = [0u8; 32];
397		arr.copy_from_slice(&out);
398		arr
399	}
400
401	pub fn spec_hash(&self) -> [u8; 32] {
402		self.spec_hash
403	}
404	pub fn version(&self) -> (u16, u16, u16) {
405		(self.inner.version_major, self.inner.version_minor, self.inner.version_patch)
406	}
407}
408
409impl TBSpec for BuiltAssertSpec {
410	fn id(&self) -> &'static str {
411		self.inner.id
412	}
413	fn mode(&self) -> ExecutionMode {
414		self.inner.execution_mode
415	}
416	fn required_assertions(&self) -> &[AssertionContract] {
417		&self.contracts
418	}
419	fn expected_gate_decision(&self) -> Option<TransitStatus> {
420		self.inner.gate_decision
421	}
422	#[cfg(feature = "instrument")]
423	fn required_events(&self) -> &[crate::utils::urn::Urn<'static>] {
424		&self.inner.required_events
425	}
426	fn validate_trace(&self, _trace: &ConsumedTrace) -> Result<(), SpecViolation> {
427		#[cfg(feature = "testing-timing")]
428		{
429			if let Some(ref schedule) = self.inner.schedulability {
430				return Self::check_schedulability(schedule);
431			}
432		}
433		Ok(())
434	}
435}
436
437#[cfg(feature = "testing-timing")]
438impl BuiltAssertSpec {
439	/// Check schedulability assertion
440	fn check_schedulability(assertion: &SchedulabilityAssertion) -> Result<(), SpecViolation> {
441		use crate::testing::schedulability::{is_edf_schedulable, is_rm_schedulable};
442
443		let result = match assertion.task_set.scheduler {
444			SchedulerType::RateMonotonic => is_rm_schedulable(&assertion.task_set),
445			SchedulerType::EarliestDeadlineFirst => is_edf_schedulable(&assertion.task_set),
446		};
447
448		match result {
449			Ok(schedule_result) => {
450				let is_schedulable = schedule_result.is_schedulable;
451				if assertion.must_be_schedulable && !is_schedulable {
452					// Task set must be schedulable but isn't
453					Err(SpecViolation::SchedulabilityViolation(schedule_result))
454				} else if !assertion.must_be_schedulable && is_schedulable {
455					// Task set must NOT be schedulable but is
456					// Create a synthetic violation for this case
457					let violation = crate::testing::schedulability::TaskViolationDetail {
458						task_id: "system".to_string(),
459						message: format!(
460							"Task set is schedulable (utilization: {:.3}, bound: {:.3}) but should not be",
461							schedule_result.utilization, schedule_result.utilization_bound
462						),
463					};
464					let mut modified_result = schedule_result;
465					modified_result.violations = vec![violation];
466					Err(SpecViolation::SchedulabilityViolation(modified_result))
467				} else {
468					Ok(())
469				}
470			}
471			Err(e) => Err(SpecViolation::SchedulabilityError(e)),
472		}
473	}
474}
475
476// ---------------------------------------------------------------------------
477// Payload encoding trait (AssertEncode) for tb_assert! ergonomic payloads
478// ---------------------------------------------------------------------------
479/// Trait converting payload values into a canonical byte representation.
480/// Numeric primitives are big-endian; &str/&\[u8\]/`Vec<u8>` zero-copy.
481pub trait AssertEncode {
482	fn tb_payload_bytes(&self) -> Cow<'_, [u8]>;
483}
484// Unsigned primitives
485impl AssertEncode for u8 {
486	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
487		Cow::Owned(vec![*self])
488	}
489}
490impl AssertEncode for u16 {
491	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
492		Cow::Owned(self.to_be_bytes().to_vec())
493	}
494}
495impl AssertEncode for u32 {
496	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
497		Cow::Owned(self.to_be_bytes().to_vec())
498	}
499}
500impl AssertEncode for u64 {
501	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
502		Cow::Owned(self.to_be_bytes().to_vec())
503	}
504}
505// Signed primitives (cast)
506impl AssertEncode for i8 {
507	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
508		Cow::Owned(vec![*self as u8])
509	}
510}
511impl AssertEncode for i16 {
512	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
513		Cow::Owned((*self as u16).to_be_bytes().to_vec())
514	}
515}
516impl AssertEncode for i32 {
517	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
518		Cow::Owned((*self as u32).to_be_bytes().to_vec())
519	}
520}
521impl AssertEncode for i64 {
522	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
523		Cow::Owned((*self as u64).to_be_bytes().to_vec())
524	}
525}
526// usize/isize canonical to 64-bit
527impl AssertEncode for usize {
528	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
529		Cow::Owned((*self as u64).to_be_bytes().to_vec())
530	}
531}
532impl AssertEncode for isize {
533	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
534		Cow::Owned((*self as u64).to_be_bytes().to_vec())
535	}
536}
537// Text / bytes
538impl AssertEncode for &str {
539	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
540		Cow::Borrowed(self.as_bytes())
541	}
542}
543impl AssertEncode for str {
544	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
545		Cow::Borrowed(self.as_bytes())
546	}
547}
548impl AssertEncode for &[u8] {
549	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
550		Cow::Borrowed(self)
551	}
552}
553impl AssertEncode for Vec<u8> {
554	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
555		Cow::Borrowed(self.as_slice())
556	}
557}
558impl AssertEncode for [u8; 32] {
559	fn tb_payload_bytes(&self) -> Cow<'_, [u8]> {
560		Cow::Borrowed(self)
561	}
562}
563
564pub fn __encode_payload<T: AssertEncode + ?Sized>(v: &T) -> Cow<'_, [u8]> {
565	T::tb_payload_bytes(v)
566}
567
568// ---------------------------------------------------------------------------
569// Helper macros
570// ---------------------------------------------------------------------------
571
572// Cardinality helper macros (thin wrappers over const fns)
573#[macro_export]
574macro_rules! exactly {
575	($n:expr) => {
576		$crate::testing::macros::Cardinality::exactly($n)
577	};
578}
579#[macro_export]
580macro_rules! at_least {
581	($n:expr) => {
582		$crate::testing::macros::Cardinality::at_least($n)
583	};
584}
585#[macro_export]
586macro_rules! at_most {
587	($n:expr) => {
588		$crate::testing::macros::Cardinality::at_most($n)
589	};
590}
591#[macro_export]
592macro_rules! between {
593	($min:expr, $max:expr) => {
594		$crate::testing::macros::Cardinality::between($min, $max)
595	};
596}
597#[macro_export]
598macro_rules! present {
599	() => {
600		$crate::testing::macros::Cardinality::present()
601	};
602}
603#[macro_export]
604macro_rules! absent {
605	() => {
606		$crate::testing::macros::Cardinality::absent()
607	};
608}
609
610// Label declaration macro
611// Usage:
612
613// Helper to build all specs (handles std vs non-std)
614#[doc(hidden)]
615#[macro_export]
616macro_rules! __tb_assert_spec_build_all {
617	(
618		$base:ident,
619		$desc_opt:expr,
620		$(
621			$maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
622			assertions: [ $( $assertion:tt ),* ],
623			$(
624				events: [ $($events_tt:tt)* ],
625			)?
626			$(, tag_filter: [ $( $tag:expr ),* $(,)? ])?
627			$(, schedulability: { $($schedule_content:tt)* })?
628		)+
629	) => {{
630		#[cfg(feature = "std")]
631		{
632			static CELL: std::sync::OnceLock<Vec<$crate::testing::macros::BuiltAssertSpec>> = std::sync::OnceLock::new();
633			CELL.get_or_init(|| {
634				let mut v = Vec::new();
635				$crate::__tb_assert_spec_build_all_impl!(
636					v, $base, $desc_opt,
637					$(
638						$maj, $min, $patch, $mode, $gate,
639						assertions: [ $( $assertion ),* ],
640						$(
641							events: [ $($events_tt)* ],
642						)?
643						$(, tag_filter: [ $( $tag ),* ])?
644						$(, schedulability: { $($schedule_content)* })?
645					)+
646				);
647				v
648			}).as_slice()
649		}
650		#[cfg(not(feature = "std"))]
651		{
652			use core::sync::atomic::{AtomicBool, Ordering};
653			static INIT: AtomicBool = AtomicBool::new(false);
654			static mut VEC: Option<Vec<$crate::testing::macros::BuiltAssertSpec>> = None;
655			if !INIT.load(Ordering::Acquire) {
656				let desc_opt = $desc_opt;
657				let mut v = Vec::new();
658				$crate::__tb_assert_spec_build_all_impl!(
659					v, $base, desc_opt,
660					$(
661						$maj, $min, $patch, $mode, $gate,
662						assertions: [ $( $assertion ),* ],
663						$(
664							events: [ $($events_tt)* ],
665						)?
666						$(, tag_filter: [ $( $tag ),* ])?
667						$(, schedulability: { $($schedule_content)* })?
668					)+
669				);
670				unsafe { VEC = Some(v); }
671				INIT.store(true, Ordering::Release);
672			}
673			unsafe { VEC.as_ref().unwrap().as_slice() }
674		}
675	}};
676}
677
678// Implementation helper to avoid nested repetition issues
679#[doc(hidden)]
680#[macro_export]
681macro_rules! __tb_assert_spec_build_all_impl {
682	(
683		$vec:ident, $base:ident, $desc_opt:expr,
684		$(
685			$maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
686			assertions: [ $( $assertion:tt ),* ],
687			$(
688				events: [ $($events_tt:tt)* ],
689			)?
690			$(, tag_filter: [ $( $tag:expr ),* $(,)? ])?
691			$(, schedulability: { $($schedule_content:tt)* })?
692		)+
693	) => {
694		$(
695			$crate::__tb_assert_spec_build_all_impl_with_events!(
696				$vec, $base, $desc_opt, $maj, $min, $patch, $mode, $gate,
697				assertions: [ $( $assertion ),* ],
698				$(
699					events: [ $($events_tt:tt)* ],
700				)?
701				$(, tag_filter: [ $( $tag ),* ])?
702				$(, schedulability: { $($schedule_content)* })?
703			);
704		)+
705	};
706}
707
708// Helper to handle events in __tb_assert_spec_build_all_impl (avoids nested repetition)
709#[doc(hidden)]
710#[macro_export]
711macro_rules! __tb_assert_spec_build_all_impl_with_events {
712	(
713		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
714		assertions: [ $( $assertion:tt ),* ],
715		$(
716			events: [ $($events_tt:tt)* ],
717		)?
718		$(, tag_filter: [ $( $tag:expr ),* ])?
719		$(, schedulability: { $($schedule_content:tt)* })?
720	) => {{
721		// Use @expand_events which handles nested repetition correctly
722		// Description is handled separately in @build_with_events via $desc_opt parameter
723		$crate::__tb_assert_spec_build! {
724			@expand_events
725			$vec, $base, $desc_opt, $maj, $min, $patch, $mode, $gate,
726			assertions: [ $( $assertion ),* ],
727			$(
728				events_tt: [ $($events_tt:tt)* ],
729			)?
730			$( tag_filter: [ $( $tag ),* ])?
731			$(, schedulability: { $($schedule_content)* })?
732		}
733	}};
734}
735
736// Helper macro for common spec building logic (single implementation)
737#[doc(hidden)]
738#[macro_export]
739macro_rules! __tb_assert_spec_build {
740	(
741		$vec:ident,
742		$base:ident,
743		$maj:literal, $min:literal, $patch:literal,
744		$mode:ident, $gate:ident,
745		assertions: [ $( $assertion:tt ),* $(,)? ],
746		$(
747			events: [ $($events_tt:tt)* ],
748		)?
749		$(tag_filter: [ $( $tag:expr ),* $(,)? ])?
750		$(, description: $desc:expr)?
751		$(, schedulability: { $($schedule_content:tt)* })?
752	) => {{
753		// Expand events token tree to avoid nested repetition issues
754		$crate::__tb_assert_spec_build! {
755			@expand_events
756			$vec, $base, $maj, $min, $patch, $mode, $gate,
757			assertions: [ $( $assertion ),* ],
758			$(
759				events_tt: [ $($events_tt:tt)* ],
760			)?
761			$( tag_filter: [ $( $tag ),* $(,)? ])?
762			$(, description: $desc)?
763			$(, schedulability: { $($schedule_content)* })?
764		}
765	}};
766	// Expand events and build - separate arms for events vs no events
767	// Pattern with desc_opt parameter (from __tb_assert_spec_build_all_impl_with_events)
768	// Has events case
769	(@expand_events
770		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
771		assertions: [ $( $assertion:tt ),* ],
772		events_tt: [ $($events_tt:tt)* ],
773		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
774		$(, schedulability: { $($schedule_content:tt)* })?
775	) => {
776		$crate::__tb_assert_spec_build! {
777			@build_with_events_expanded
778			$vec, $base, $desc_opt, $maj, $min, $patch, $mode, $gate,
779			assertions: [ $( $assertion ),* ],
780			events_tt: [ $($events_tt:tt)* ],
781			$( tag_filter: [ $( $tag ),* $(,)? ])?
782			$(, schedulability: { $($schedule_content)* })?
783		}
784	};
785	// No events case
786	(@expand_events
787		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
788		assertions: [ $( $assertion:tt ),* ],
789		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
790		$(, schedulability: { $($schedule_content:tt)* })?
791	) => {
792		$crate::__tb_assert_spec_build! {
793			@build_with_events
794			$vec, $base, $desc_opt, $maj, $min, $patch, $mode, $gate,
795			assertions: [ $( $assertion ),* ],
796			events: [ ],
797			$( tag_filter: [ $( $tag ),* ])?
798			$(, schedulability: { $($schedule_content)* })?
799		}
800	};
801	// Legacy pattern without desc_opt (for backward compatibility)
802	(@expand_events
803		$vec:ident, $base:ident, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
804		assertions: [ $( $assertion:tt ),* ],
805		$(
806			events_tt: [ $($events_tt:tt)* ],
807		)?
808		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
809		$(, description: $desc:expr)?
810		$(, schedulability: { $($schedule_content:tt)* })?
811	) => {
812		$(
813			// Has events - expand events_tt here
814			$crate::__tb_assert_spec_build! {
815				@build_with_events_expanded
816				$vec, $base, None, $maj, $min, $patch, $mode, $gate,
817				assertions: [ $( $assertion ),* ],
818				events_tt: [ $($events_tt:tt)* ],
819				$( tag_filter: [ $( $tag ),* $(,)? ])?
820				$(, description: $desc)?
821				$(, schedulability: { $($schedule_content)* })?
822			}
823		)?
824		$(
825			// No events case
826			$crate::__tb_assert_spec_build! {
827				@build_with_events
828				$vec, $base, None, $maj, $min, $patch, $mode, $gate,
829				assertions: [ $( $assertion ),* ],
830				events: [ ],
831				$( tag_filter: [ $( $tag ),* $(,)? ])?
832				$(, description: $desc)?
833				$(, schedulability: { $($schedule_content)* })?
834			}
835		)?
836	};
837	(@build_with_events_expanded
838		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
839		assertions: [ $( $assertion:tt ),* ],
840		events_tt: [ $( $ev:ident ),* $(,)? ],
841		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
842		$(, description: $desc:expr)?
843		$(, schedulability: { $($schedule_content:tt)* })?
844	) => {{
845		$crate::__tb_assert_spec_build! {
846			@build_with_events
847			$vec, $base, $desc_opt, $maj, $min, $patch, $mode, $gate,
848			assertions: [ $( $assertion ),* ],
849			events: [ $( $ev ),* $(,)? ],
850			$( tag_filter: [ $( $tag ),* $(,)? ])?
851			$(, description: $desc)?
852			$(, schedulability: { $($schedule_content)* })?
853		}
854	}};
855	// Handle token tree events (from __tb_assert_spec_build_all_impl_with_events)
856	(@build_with_events_expanded
857		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
858		assertions: [ $( $assertion:tt ),* ],
859		events_tt: [ $($events_tt:tt)* ],
860		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
861		$(, description: $desc:expr)?
862		$(, schedulability: { $($schedule_content:tt)* })?
863	) => {{
864		// Recursively expand token tree to identifiers
865		$crate::__tb_assert_spec_build! {
866			@build_with_events_expanded
867			$vec, $base, $desc_opt, $maj, $min, $patch, $mode, $gate,
868			assertions: [ $( $assertion ),* ],
869			events_tt: [ $($events_tt)* ],
870			$( tag_filter: [ $( $tag ),* $(,)? ])?
871			$(, description: $desc)?
872			$(, schedulability: { $($schedule_content)* })?
873		}
874	}};
875	// Legacy pattern without desc_opt (for backward compatibility)
876	(@build_with_events_expanded
877		$vec:ident, $base:ident, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
878		assertions: [ $( $assertion:tt ),* ],
879		events_tt: [ $($events_tt:tt)* ],
880		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
881		$(, description: $desc:expr)?
882		$(, schedulability: { $($schedule_content:tt)* })?
883	) => {{
884		// Recursively expand token tree to identifiers
885		$crate::__tb_assert_spec_build! {
886			@build_with_events_expanded
887			$vec, $base, None, $maj, $min, $patch, $mode, $gate,
888			assertions: [ $( $assertion ),* ],
889			events_tt: [ $($events_tt)* ],
890			$( tag_filter: [ $( $tag ),* $(,)? ])?
891			$(, description: $desc)?
892			$(, schedulability: { $($schedule_content)* })?
893		}
894	}};
895	(@build_with_events
896		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
897		assertions: [ $( $assertion:tt ),* ],
898		events: [ $( $ev:expr ),* $(,)? ],
899		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
900		$(, description: $desc:expr)?
901		$(, schedulability: { $($schedule_content:tt)* })?
902	) => {{
903		let mut builder = $crate::__tb_assert_spec_init_builder!(
904			$base, $desc_opt, $maj, $min, $patch, $mode, $gate,
905			$(tag_filter: [ $( $tag ),* ])?
906			$(, description: $desc)?
907		);
908		$(
909			builder = $crate::__tb_assert_spec_add_assertion!(builder, $assertion);
910		)*
911		#[cfg(feature = "instrument")]
912		{
913			$(
914				builder = builder.required_events(&[$ev.clone()]);
915			)*
916		}
917		$(
918			#[cfg(feature = "testing-timing")]
919			{
920				$crate::__tb_assert_spec_parse_schedulability!(builder, $($schedule_content)*);
921			}
922		)?
923		$vec.push(builder.build());
924	}};
925	// Empty events case
926	(@expand_events
927		$vec:ident, $base:ident, $desc_opt:expr, $maj:literal, $min:literal, $patch:literal, $mode:ident, $gate:ident,
928		assertions: [ $( $assertion:tt ),* ],
929		events_tt: [ ],
930		$( tag_filter: [ $( $tag:expr ),* $(,)? ])?
931		$(, description: $desc:expr)?
932		$(, schedulability: { $($schedule_content:tt)* })?
933	) => {{
934		let mut builder = $crate::__tb_assert_spec_init_builder!(
935			$base, $desc_opt, $maj, $min, $patch, $mode, $gate,
936			$(tag_filter: [ $( $tag ),* ])?
937			$(, description: $desc)?
938		);
939		$(
940			builder = $crate::__tb_assert_spec_add_assertion!(builder, $assertion);
941		)*
942		$(
943			#[cfg(feature = "testing-timing")]
944			{
945				$crate::__tb_assert_spec_parse_schedulability!(builder, $($schedule_content)*);
946			}
947		)?
948		$vec.push(builder.build());
949	}};
950}
951
952// Helper to parse schedulability assertions
953#[doc(hidden)]
954#[cfg(feature = "testing-timing")]
955#[macro_export]
956macro_rules! __tb_assert_spec_parse_schedulability {
957	(
958		$builder:ident,
959		task_set: $task_set:expr,
960		scheduler: $scheduler:ident,
961		must_be_schedulable: $must_be:expr,
962	) => {{
963		use $crate::testing::schedulability::SchedulerType;
964		let task_set_with_scheduler = {
965			let mut ts = $task_set.clone();
966			ts.scheduler = SchedulerType::$scheduler;
967			ts
968		};
969		let assertion = $crate::testing::macros::SchedulabilityAssertion {
970			task_set: task_set_with_scheduler,
971			must_be_schedulable: $must_be,
972		};
973		$builder = $builder.schedulability(assertion);
974	}};
975}
976
977// Helper to add individual assertions (handles tags and values)
978#[doc(hidden)]
979#[macro_export]
980macro_rules! __tb_assert_spec_add_assertion {
981	// NEW SYNTAX: With value and tags - match equals! specifically
982	($builder:expr, ($label:expr, $card:expr, equals!($value:expr), tags: [ $($tag:expr),* $(,)? ])) => {
983		$builder
984			.assertion_with_value($label, vec![ $($tag),* ], $card, Some($crate::testing::assertions::AssertionValue::from($value)))
985			.expect("duplicate label or invalid range")
986	};
987	// NEW SYNTAX: With value, no tags - match equals! specifically
988	($builder:expr, ($label:expr, $card:expr, equals!($value:expr))) => {
989		$builder
990			.assertion_with_value($label, vec![], $card, Some($crate::testing::assertions::AssertionValue::from($value)))
991			.expect("duplicate label or invalid range")
992	};
993	// NEW SYNTAX: With tags, no value
994	($builder:expr, ($label:expr, $card:expr, tags: [ $($tag:expr),* $(,)? ])) => {
995		$builder
996			.assertion($label, vec![ $($tag),* ], $card)
997			.expect("duplicate label or invalid range")
998	};
999	// NEW SYNTAX: No tags, no value (2-element tuple)
1000	($builder:expr, ($label:expr, $card:expr)) => {
1001		$builder
1002			.assertion($label, vec![], $card)
1003			.expect("duplicate label or invalid range")
1004	};
1005}
1006
1007// Multi-version macro ONLY (full semantic version required maj.min.patch)
1008#[macro_export]
1009macro_rules! tb_assert_spec {
1010	(
1011		$(#[$meta:meta])*
1012		$vis:vis $base:ident,
1013		$( V ( $maj:literal , $min:literal , $patch:literal ) : {
1014			mode: $mode:ident,
1015			gate: $gate:ident,
1016			$( tag_filter: [ $( $tag:expr ),* $(,)? ], )?
1017			assertions: [ $( $assertion:tt ),* $(,)? ]
1018			$(, events: [ $($events_tt:tt)* ])?
1019			$(, schedulability: { $($schedule_content:tt)* })?
1020		} ),+ $(,)?
1021		$(, annotations { description: $desc:expr })?
1022	) => {
1023		$(#[$meta])*
1024		$vis struct $base;
1025		impl $base {
1026			pub fn all() -> &'static [$crate::testing::macros::BuiltAssertSpec] {
1027				let desc_opt: Option<&'static str> = None::<&'static str> $(.or(Some($desc)))?;
1028				$crate::__tb_assert_spec_build_all!(
1029					$base,
1030					desc_opt,
1031					$(
1032						$maj, $min, $patch, $mode, $gate,
1033						assertions: [ $( $assertion ),* ],
1034						$(
1035							events: [ $($events_tt:tt)* ],
1036						)?
1037						$(, tag_filter: [ $( $tag ),* ])?
1038						$(, schedulability: { $($schedule_content)* })?
1039					)+
1040				)
1041			}
1042
1043			#[allow(dead_code)]
1044			pub fn get(maj: u16, min: u16, patch: u16) -> Option<&'static $crate::testing::macros::BuiltAssertSpec> {
1045				for s in Self::all() {
1046					let (maj_ver, min_ver, patch_ver) = s.version();
1047					if maj_ver == maj && min_ver == min && patch_ver == patch {
1048						return Some(s);
1049					}
1050				}
1051				None
1052			}
1053
1054			#[allow(dead_code)]
1055			pub fn latest() -> &'static $crate::testing::macros::BuiltAssertSpec {
1056				let mut best: Option<&'static $crate::testing::macros::BuiltAssertSpec> = None;
1057				for s in Self::all() {
1058					match best {
1059						Some(b) => if s.version() > b.version() { best = Some(s); },
1060						None => best = Some(s)
1061					}
1062				}
1063				best.expect("no versions defined")
1064			}
1065		}
1066	};
1067}
1068
1069// ---------------------------------------------------------------------------
1070// Scenario macro MVP
1071// ---------------------------------------------------------------------------
1072// Scenario macro MVP: Worker & Bare variants (ServiceClient stubbed)
1073// ---------------------------------------------------------------------------
1074
1075// Helper to validate CSP and FDR (reduces duplication)
1076#[doc(hidden)]
1077#[macro_export]
1078macro_rules! __tb_scenario_validate_csp_fdr {
1079	(
1080		$trace:expr,
1081		$(csp: $csp:ty,)?
1082		$(fdr: $fdr_config:expr,)?
1083	) => {{
1084		// CSP validation if provided
1085		#[cfg(feature = "testing-csp")]
1086		let csp_result: Option<$crate::testing::specs::csp::CspValidationResult> = {
1087			$crate::tb_scenario!(@csp_validate $trace, $($csp)?)
1088		};
1089
1090		#[cfg(not(feature = "testing-csp"))]
1091		let csp_result: Option<$crate::testing::specs::csp::CspValidationResult> = None;
1092
1093		// Check if CSP validation failed
1094		#[cfg(feature = "testing-csp")]
1095		let csp_failed = csp_result.as_ref().map(|r| !r.valid).unwrap_or(false);
1096		#[cfg(not(feature = "testing-csp"))]
1097		let csp_failed = false;
1098
1099		// FDR validation if provided
1100		#[cfg(feature = "testing-fdr")]
1101		let (fdr_result, fdr_config): (Option<$crate::testing::fdr::FdrVerdict>, Option<$crate::testing::fdr::FdrConfig>) = {
1102			$crate::tb_scenario!(@fdr_validate_with_config $trace, $($fdr_config)?)
1103		};
1104
1105		#[cfg(not(feature = "testing-fdr"))]
1106		let (fdr_result, fdr_config): (Option<$crate::testing::fdr::FdrVerdict>, Option<$crate::testing::fdr::FdrConfig>) = (None, None);
1107
1108		// Check if FDR validation failed
1109		#[cfg(feature = "testing-fdr")]
1110		let fdr_failed = fdr_result.as_ref().map(|v| !v.passed).unwrap_or(false);
1111		#[cfg(not(feature = "testing-fdr"))]
1112		let fdr_failed = false;
1113
1114		// Check if FDR failure is expected (for negative tests)
1115		#[cfg(feature = "testing-fdr")]
1116		let expect_failure = fdr_config.as_ref().map(|c| c.expect_failure).unwrap_or(false);
1117		#[cfg(not(feature = "testing-fdr"))]
1118		let expect_failure = false;
1119
1120		(csp_result, csp_failed, fdr_result, fdr_config, fdr_failed, expect_failure)
1121	}};
1122}
1123
1124// Helper macro to call hooks and handle results (reduces duplication)
1125#[doc(hidden)]
1126#[macro_export]
1127macro_rules! __tb_scenario_call_hooks {
1128	(
1129		scenario_result: $scenario_result:expr,
1130		csp_failed: $csp_failed:expr,
1131		fdr_failed: $fdr_failed:expr,
1132		expect_failure: $expect_failure:expr,
1133		$(hooks: {
1134			$(on_pass: $on_pass:expr,)?
1135			$(on_fail: $on_fail:expr)?
1136		},)?
1137	) => {{
1138		#[allow(unreachable_code)]
1139		#[allow(unused_labels)]
1140		let hook_result: Result<(), Box<dyn std::error::Error>> = 'hook_call: {
1141			if $scenario_result.passed {
1142				// Test passed - call on_pass hook if provided
1143				$(
1144					$(
1145						fn __call_hook<F>(f: F, trace: &$crate::trace::ConsumedTrace, result: &$crate::testing::ScenarioResult) -> Result<(), Box<dyn std::error::Error>>
1146						where
1147							F: FnOnce(&$crate::trace::ConsumedTrace, &$crate::testing::ScenarioResult) -> Result<(), Box<dyn std::error::Error>>,
1148						{
1149							f(trace, result)
1150						}
1151						break 'hook_call __call_hook($on_pass, &$scenario_result.trace, &$scenario_result);
1152					)?
1153				)?
1154				Ok(())
1155			} else {
1156				// Test failed - call on_fail hook if provided
1157				$(
1158					$(
1159						fn __call_hook<F>(f: F, trace: &$crate::trace::ConsumedTrace, result: &$crate::testing::ScenarioResult) -> Result<(), Box<dyn std::error::Error>>
1160						where
1161							F: FnOnce(&$crate::trace::ConsumedTrace, &$crate::testing::ScenarioResult) -> Result<(), Box<dyn std::error::Error>>,
1162						{
1163							f(trace, result)
1164						}
1165						break 'hook_call __call_hook($on_fail, &$scenario_result.trace, &$scenario_result);
1166					)?
1167				)?
1168				// No hook provided
1169				Err(format!("{}", $scenario_result).into())
1170			}
1171		};
1172		hook_result
1173	}};
1174}
1175
1176// Helper macro for common spec builder initialization (reduces duplication)
1177#[doc(hidden)]
1178#[macro_export]
1179macro_rules! __tb_assert_spec_init_builder {
1180	(
1181		$base:ident,
1182		$desc_opt:expr,
1183		$maj:literal,
1184		$min:literal,
1185		$patch:literal,
1186		$mode:ident,
1187		$gate:ident,
1188		$(tag_filter: [ $($tag:expr),* $(,)? ])?
1189		$(, description: $desc:expr)?
1190	) => {{
1191		let (maj, min, patch) = ($maj as u16, $min as u16, $patch as u16);
1192		let mut builder = $crate::testing::macros::AssertSpecBuilder::new(
1193			stringify!($base),
1194			$crate::trace::ExecutionMode::$mode,
1195		);
1196		builder = builder.version(maj, min, patch).gate_decision($crate::policy::TransitStatus::$gate);
1197		$(
1198			builder = builder.tag_filter(vec![ $( $tag ),* ]);
1199		)?
1200		$(
1201			if let Some(desc) = $desc {
1202				builder = builder.description(desc);
1203			}
1204		)?
1205		// Handle description from desc_opt parameter
1206		if let Some(desc) = $desc_opt {
1207			builder = builder.description(desc);
1208		}
1209		builder
1210	}};
1211}
1212
1213/// Helper macro for common trace verification logic (reduces duplication)
1214#[doc(hidden)]
1215#[macro_export]
1216macro_rules! __tb_scenario_verify_impl {
1217	// Single spec variant with optional CSP and FDR
1218	(
1219		single_spec: $spec:ty,
1220		trace: $trace:expr,
1221		$(csp: $csp:ty,)?
1222		$(fdr: $fdr_config:expr,)?
1223		$(hooks: {
1224			$(on_pass: $on_pass:expr,)?
1225			$(on_fail: $on_fail:expr)?
1226		},)?
1227	) => {{
1228		let spec = <$spec>::latest();
1229		let l1_result = $crate::testing::specs::verify_trace(spec, &$trace);
1230
1231		// Build ScenarioResult with all layer results
1232		let mut scenario_result = $crate::testing::ScenarioResult::default();
1233		// Move trace into result (transfer ownership)
1234		scenario_result.trace = $trace;
1235		// Clone and store the spec
1236		scenario_result.assert_spec = Some(spec.clone());
1237		// Layer 1: Spec verification
1238		scenario_result.spec_violation = l1_result.as_ref().err().cloned();
1239
1240		let l1_passed = l1_result.is_ok();
1241
1242		// Delegate to common implementation
1243		$crate::__tb_scenario_verify_impl! {
1244			@common
1245			l1_passed: l1_passed,
1246			scenario_result: scenario_result,
1247			$(csp: $csp,)?
1248			$(fdr: $fdr_config,)?
1249			$(hooks: {
1250				$(on_pass: $on_pass,)?
1251				$(on_fail: $on_fail)?
1252			},)?
1253		}
1254	}};
1255
1256	// Multiple specs variant with optional CSP and FDR
1257	(
1258		multi_specs: $specs:expr,
1259		trace: $trace:expr,
1260		$(csp: $csp:ty,)?
1261		$(fdr: $fdr_config:expr,)?
1262		$(hooks: {
1263			$(on_pass: $on_pass:expr,)?
1264			$(on_fail: $on_fail:expr)?
1265		},)?
1266	) => {{
1267		let mut all_passed = true;
1268		let mut first_violation = None;
1269
1270		// Validate all specs
1271		for spec in &$specs {
1272			let verification_result = $crate::testing::specs::verify_trace(*spec, &$trace);
1273			if let Err(v) = verification_result {
1274				all_passed = false;
1275				if first_violation.is_none() {
1276					first_violation = Some(v);
1277				}
1278			}
1279		}
1280
1281		// Build ScenarioResult
1282		let mut scenario_result = $crate::testing::ScenarioResult::default();
1283
1284		// Move trace into result
1285		scenario_result.trace = $trace;
1286		// Clone and store all specs
1287		scenario_result.assert_specs = $specs.iter().map(|s| (*s).clone()).collect();
1288		scenario_result.spec_violation = first_violation;
1289
1290		// Delegate to common implementation
1291		$crate::__tb_scenario_verify_impl! {
1292			@common
1293			l1_passed: all_passed,
1294			scenario_result: scenario_result,
1295			$(csp: $csp,)?
1296			$(fdr: $fdr_config,)?
1297			$(hooks: {
1298				$(on_pass: $on_pass,)?
1299				$(on_fail: $on_fail)?
1300			},)?
1301			}
1302	}};
1303
1304	// Common implementation for both single and multiple specs
1305	(
1306		@common
1307		l1_passed: $l1_passed:expr,
1308		scenario_result: $scenario_result:expr,
1309		$(csp: $csp:ty,)?
1310		$(fdr: $fdr_config:expr,)?
1311		$(hooks: {
1312			$(on_pass: $on_pass:expr,)?
1313			$(on_fail: $on_fail:expr)?
1314		},)?
1315	) => {{
1316		let mut scenario_result = $scenario_result;
1317		let l1_passed = $l1_passed;
1318
1319		// Layer 2: CSP validation (if provided)
1320		#[allow(unused_mut, unused_assignments)]
1321		let mut csp_failed = false;
1322
1323		#[cfg(feature = "testing-csp")]
1324		{
1325			$(
1326				let csp_spec = <$csp>::default();
1327				let csp_result = <$csp as $crate::testing::specs::csp::ProcessSpec>::validate_trace(&csp_spec, &scenario_result.trace);
1328				csp_failed = !csp_result.valid;
1329				scenario_result.csp_result = Some(csp_result);
1330				// Move process into result
1331				scenario_result.process = Some(<$csp>::process());
1332
1333				// Move timing constraints into result (if available)
1334				#[cfg(feature = "testing-timing")]
1335				{
1336					let process = scenario_result.process.as_ref().unwrap();
1337					scenario_result.timing_constraints = process.timing_constraints.clone();
1338				}
1339			)?
1340		}
1341
1342		// Layer 3: FDR validation (if provided)
1343		#[allow(unused_mut, unused_assignments)]
1344		let mut fdr_failed = false;
1345		#[allow(unused_mut, unused_assignments)]
1346		let mut expect_failure = false;
1347
1348		#[cfg(feature = "testing-fdr")]
1349		{
1350			$(
1351				use $crate::testing::fdr::{DefaultFdrExplorer, FdrConfig};
1352				let config: FdrConfig = $fdr_config.into();
1353				expect_failure = config.expect_failure;
1354
1355				// AUTOMATIC MODE SELECTION:
1356				// If fault_model + specs provided → explore spec WITH faults (specification robustness)
1357				// Otherwise → explore execution trace (normal behavior / implementation resilience)
1358				#[cfg(feature = "testing-fault")]
1359				let process_to_explore = if config.fault_model.is_some() && !config.specs.is_empty() {
1360					&config.specs[0]
1361				} else {
1362					&scenario_result.trace.to_process()
1363				};
1364
1365				#[cfg(not(feature = "testing-fault"))]
1366				let process_to_explore = &scenario_result.trace.to_process();
1367
1368				let mut explorer = DefaultFdrExplorer::with_defaults(process_to_explore, config.clone());
1369				let verdict = explorer.explore();
1370				fdr_failed = !verdict.passed;
1371				scenario_result.fdr_verdict = Some(verdict);
1372			)?
1373		}
1374
1375		// Determine overall pass/fail
1376		scenario_result.passed = l1_passed && !csp_failed && (!fdr_failed || expect_failure);
1377
1378		// Call hooks and get their decision
1379		$crate::__tb_scenario_call_hooks!(
1380			scenario_result: scenario_result,
1381			csp_failed: csp_failed,
1382			fdr_failed: fdr_failed,
1383			expect_failure: expect_failure,
1384			$(hooks: {
1385				$(on_pass: $on_pass,)?
1386				$(on_fail: $on_fail)?
1387			},)?
1388		)
1389	}};
1390}