1use alloc::boxed::Box;
10use alloc::string::String;
11use core::time::Duration;
12
13use crate::diagnostics::{DiagnosticEvent, Observer};
14use crate::operation::StageOperation;
15use crate::schedule::stage;
16pub use crate::schedule::BuildError;
17use crate::schedule::RunOutcome;
18use crate::schedule::{
19 FlushMode, RunContext, Schedule, ScheduleBuilder, ScheduleError, System, SystemId, SystemSet,
20 UpdatePlan,
21};
22use crate::time::{FixedConfig, FixedWork};
23use crate::world::{World, WorldBuilder};
24
25#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct AppFault {
28 pub stage: Option<String>,
30 pub system: Option<String>,
32 pub detail: Option<String>,
34}
35
36#[non_exhaustive]
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub enum AppError {
40 InvalidDelta,
42 PendingIdleCommands,
44 WorldMutationPoisoned,
46 TerminalFault,
48 WorldTickExhausted,
50 FixedStepExhausted,
52 InvalidUpdatePlan(ScheduleError),
54 Fault(AppFault),
56}
57
58pub struct App {
60 world: World,
61 schedule: Schedule,
62 run_context: RunContext,
63 faulted: bool,
64 fault: Option<AppFault>,
65 observer: Option<Box<dyn Observer>>,
66}
67
68pub struct AppBuilder {
70 world_builder: WorldBuilder,
71 schedule_builder: ScheduleBuilder,
72 observer: Option<Box<dyn Observer>>,
73}
74
75impl App {
76 pub fn builder() -> AppBuilder {
78 AppBuilder::new()
79 }
80
81 pub fn from_parts(world: World, schedule: Schedule) -> Result<Self, BuildError> {
85 if world.has_pending_commands() {
86 return Err(BuildError::PendingCommands);
87 }
88 if !world.run_guard_is_idle() {
89 return Err(BuildError::WorldRunning);
90 }
91 if world.is_mutation_poisoned() {
92 return Err(BuildError::WorldMutationPoisoned);
93 }
94 if !world.validate_execution_lease(schedule.execution_lease()) {
95 return Err(BuildError::LeaseMismatch);
96 }
97 let set_count = schedule.set_count();
98 Ok(Self {
99 world,
100 schedule,
101 run_context: RunContext::with_set_capacity(set_count),
102 faulted: false,
103 fault: None,
104 observer: None,
105 })
106 }
107
108 pub fn world(&self) -> &World {
110 &self.world
111 }
112
113 pub fn world_mut(&mut self) -> &mut World {
115 &mut self.world
116 }
117
118 pub fn schedule(&self) -> &Schedule {
120 &self.schedule
121 }
122
123 pub fn is_faulted(&self) -> bool {
125 self.faulted
126 }
127
128 pub fn fault(&self) -> Option<&AppFault> {
130 self.fault.as_ref()
131 }
132
133 pub fn set_system_enabled(
135 &mut self,
136 id: &SystemId,
137 enabled: bool,
138 ) -> Result<(), ScheduleError> {
139 self.schedule.set_system_enabled(id, enabled)
140 }
141
142 pub fn update(&mut self, delta_seconds: f32) -> Result<(), AppError> {
144 self.update_with(delta_seconds, |_| ())
145 }
146
147 pub fn update_plan(&mut self, delta_seconds: f32, plan: &UpdatePlan) -> Result<(), AppError> {
153 self.update_inner(delta_seconds, Some(plan), |_| ())
154 }
155
156 pub fn update_with<R>(
158 &mut self,
159 delta_seconds: f32,
160 observe: impl FnOnce(&World) -> R,
161 ) -> Result<R, AppError> {
162 self.update_inner(delta_seconds, None, observe)
163 }
164
165 fn update_inner<R>(
166 &mut self,
167 delta_seconds: f32,
168 plan: Option<&UpdatePlan>,
169 observe: impl FnOnce(&World) -> R,
170 ) -> Result<R, AppError> {
171 if let Some(plan) = plan {
172 self.schedule
173 .validate_update_plan(plan)
174 .map_err(AppError::InvalidUpdatePlan)?;
175 }
176 self.ensure_ready()?;
177 validate_delta(delta_seconds)?;
178 emit(
179 &mut self.observer,
180 DiagnosticEvent::UpdateStart { delta_seconds },
181 );
182
183 let frame_delta = duration_from_seconds(delta_seconds)?;
184 let fixed_stage_selected = plan.map_or(true, |plan| {
185 self.schedule
186 .update_stage_indices()
187 .iter()
188 .copied()
189 .any(|index| {
190 self.schedule.stage_label_at(index) == stage::FIXED_UPDATE
191 && self.schedule.plan_contains_stage(plan, index)
192 })
193 });
194 let fixed_config = self.schedule.fixed_config().copied();
195 let fixed_plan = if fixed_stage_selected {
196 if let Some(config) = fixed_config {
197 let peek = self
198 .schedule
199 .fixed_accumulator()
200 .peek_plan(frame_delta, &config);
201 let planned_steps = match peek.work {
202 FixedWork::Steps(steps) => steps as u128,
203 FixedWork::Coalesced { steps, .. } => steps,
204 };
205 self.world
206 .preflight_world_tick()
207 .map_err(|_| self.fault_tick_exhaustion())?;
208 self.schedule
209 .fixed_accumulator()
210 .preflight_steps(planned_steps)
211 .map_err(|_| self.fault_fixed_exhaustion())?;
212 let fixed_plan = self
213 .schedule
214 .fixed_accumulator_mut()
215 .plan(frame_delta, &config);
216 if let Some(debt) = fixed_plan.dropped {
217 emit(
218 &mut self.observer,
219 DiagnosticEvent::FixedDebtDropped { steps: debt.steps },
220 );
221 }
222 if let Some(debt) = fixed_plan.coalesced {
223 emit(
224 &mut self.observer,
225 DiagnosticEvent::FixedDebtCoalesced { steps: debt.steps },
226 );
227 }
228 Some(fixed_plan)
229 } else {
230 self.world
231 .preflight_world_tick()
232 .map_err(|_| self.fault_tick_exhaustion())?;
233 None
234 }
235 } else {
236 self.world
237 .preflight_world_tick()
238 .map_err(|_| self.fault_tick_exhaustion())?;
239 None
240 };
241
242 self.world
243 .advance_world_tick()
244 .map_err(|_| self.fault_tick_exhaustion())?;
245
246 self.run_context.fixed_step = None;
247 let update_stage_count = self.schedule.update_stage_indices().len();
248 for stage_order_index in 0..update_stage_count {
249 let stage_index = self.schedule.update_stage_indices()[stage_order_index];
250 let stage_label = self.schedule.stage_label_at(stage_index);
251 let selected = plan.map_or(true, |plan| {
252 self.schedule.plan_contains_stage(plan, stage_index)
253 });
254 let startup_pending = stage_label == stage::STARTUP;
255 if !selected && !startup_pending {
256 continue;
257 }
258 if stage_label == stage::FIXED_UPDATE {
259 if let Some(config) = fixed_config {
260 if let Some(fixed_plan) = fixed_plan {
261 match fixed_plan.work {
262 FixedWork::Steps(substeps) => {
263 for _ in 0..substeps {
264 let step =
265 self.schedule.fixed_accumulator_mut().next_step(&config);
266 self.world.set_fixed_step(Some(step));
267 self.run_context.fixed_step = Some(step);
268 let result = self
269 .run_stage(stage_index, seconds_from_duration(step.delta));
270 self.world.set_fixed_step(None);
271 self.run_context.fixed_step = None;
272 result?;
273 }
274 }
275 FixedWork::Coalesced { steps, delta } => {
276 let steps = u64::try_from(steps)
277 .expect("fixed-step preflight accepts coalesced step count");
278 let step = self
279 .schedule
280 .fixed_accumulator_mut()
281 .next_coalesced(steps, delta);
282 self.world.set_fixed_step(Some(step));
283 self.run_context.fixed_step = Some(step);
284 let result =
285 self.run_stage(stage_index, seconds_from_duration(delta));
286 self.world.set_fixed_step(None);
287 self.run_context.fixed_step = None;
288 result?;
289 }
290 }
291 }
292 }
293 continue;
294 }
295 self.run_stage(stage_index, delta_seconds)?;
296 }
297
298 self.run_final_flush()?;
299 let observed = self.observe_with_cleanup(StageOperation::Update, observe);
300 self.schedule
301 .clear_frame_events(&mut self.world, StageOperation::Update);
302 emit(&mut self.observer, DiagnosticEvent::UpdateFinish);
303 Ok(observed)
304 }
305
306 pub fn render(&mut self, delta_seconds: f32) -> Result<(), AppError> {
308 self.render_with(delta_seconds, |_| ())
309 }
310
311 pub fn render_with<R>(
313 &mut self,
314 delta_seconds: f32,
315 observe: impl FnOnce(&World) -> R,
316 ) -> Result<R, AppError> {
317 self.ensure_ready()?;
318 validate_delta(delta_seconds)?;
319 emit(
320 &mut self.observer,
321 DiagnosticEvent::RenderStart { delta_seconds },
322 );
323 self.run_context.fixed_step = None;
324 let run_result = {
325 let schedule = &mut self.schedule;
326 let world = &mut self.world;
327 let observer = &mut self.observer;
328 let context = &mut self.run_context;
329 let faulted = &mut self.faulted;
330 let fault = &mut self.fault;
331 catch_schedule_panic(|| {
332 with_terminal_unwind_cleanup(
333 world,
334 context,
335 faulted,
336 fault,
337 StageOperation::Render,
338 |world, context| {
339 schedule.run_operation(
340 world,
341 StageOperation::Render,
342 context,
343 delta_seconds,
344 observer,
345 )
346 },
347 )
348 })
349 };
350 handle_guarded_run(self, run_result)?;
351 let observed = self.observe_with_cleanup(StageOperation::Render, observe);
352 self.schedule
353 .clear_frame_events(&mut self.world, StageOperation::Render);
354 emit(&mut self.observer, DiagnosticEvent::RenderFinish);
355 Ok(observed)
356 }
357
358 fn ensure_ready(&self) -> Result<(), AppError> {
359 if self.faulted {
360 return Err(AppError::TerminalFault);
361 }
362 if self.world.is_mutation_poisoned() {
363 return Err(AppError::WorldMutationPoisoned);
364 }
365 if self.world.has_pending_commands() {
366 return Err(AppError::PendingIdleCommands);
367 }
368 Ok(())
369 }
370
371 fn run_stage(&mut self, stage_index: usize, dt: f32) -> Result<(), AppError> {
372 let run_result = {
373 let schedule = &mut self.schedule;
374 let world = &mut self.world;
375 let observer = &mut self.observer;
376 let context = &mut self.run_context;
377 let faulted = &mut self.faulted;
378 let fault = &mut self.fault;
379 catch_schedule_panic(|| {
380 with_terminal_unwind_cleanup(
381 world,
382 context,
383 faulted,
384 fault,
385 StageOperation::Update,
386 |world, context| schedule.run_stage(world, stage_index, context, dt, observer),
387 )
388 })
389 };
390 handle_guarded_run(self, run_result)
391 }
392
393 fn observe_with_cleanup<R>(
394 &mut self,
395 operation: StageOperation,
396 observe: impl FnOnce(&World) -> R,
397 ) -> R {
398 let run_result = {
399 let world = &mut self.world;
400 let context = &mut self.run_context;
401 let faulted = &mut self.faulted;
402 let fault = &mut self.fault;
403 catch_schedule_panic(|| {
404 with_terminal_unwind_cleanup(
405 world,
406 context,
407 faulted,
408 fault,
409 operation,
410 |world, _context| observe(world),
411 )
412 })
413 };
414 handle_guarded_value(self, run_result)
415 }
416
417 fn fault_tick_exhaustion(&mut self) -> AppError {
418 self.record_exhaustion_fault("world tick exhausted");
419 AppError::WorldTickExhausted
420 }
421
422 fn fault_fixed_exhaustion(&mut self) -> AppError {
423 self.record_exhaustion_fault("fixed step exhausted");
424 AppError::FixedStepExhausted
425 }
426
427 fn record_exhaustion_fault(&mut self, detail: &str) {
428 self.faulted = true;
429 if self.fault.is_none() {
430 self.fault = Some(AppFault {
431 stage: None,
432 system: None,
433 detail: Some(String::from(detail)),
434 });
435 }
436 self.world.set_fixed_step(None);
437 self.run_context.fixed_step = None;
438 let _ = self.world.discard_commands();
439 self.world.end_run();
440 emit(
441 &mut self.observer,
442 DiagnosticEvent::Fault {
443 stage: None,
444 system: None,
445 },
446 );
447 }
448
449 fn run_final_flush(&mut self) -> Result<(), AppError> {
450 let run_result = {
451 let schedule = &mut self.schedule;
452 let world = &mut self.world;
453 let observer = &mut self.observer;
454 let context = &mut self.run_context;
455 let faulted = &mut self.faulted;
456 let fault = &mut self.fault;
457 catch_schedule_panic(|| {
458 with_terminal_unwind_cleanup(
459 world,
460 context,
461 faulted,
462 fault,
463 StageOperation::Update,
464 |world, _context| schedule.run_final_update_flush(world, observer),
465 )
466 })
467 };
468 handle_guarded_run(self, run_result)
469 }
470
471 fn fault_from(&mut self, outcome: RunOutcome) -> AppError {
472 self.record_fault(&outcome);
473 AppError::Fault(AppFault {
474 stage: outcome.fault_stage,
475 system: outcome.fault_system,
476 detail: outcome.fault_detail,
477 })
478 }
479
480 fn record_fault(&mut self, outcome: &RunOutcome) {
481 self.faulted = true;
482 if self.fault.is_none() {
483 self.fault = Some(AppFault {
484 stage: outcome.fault_stage.clone(),
485 system: outcome.fault_system.clone(),
486 detail: outcome.fault_detail.clone(),
487 });
488 }
489 let _ = self.world.discard_commands();
490 self.world.set_fixed_step(None);
491 self.run_context.fixed_step = None;
492 self.world.end_run();
493 emit(
494 &mut self.observer,
495 DiagnosticEvent::Fault {
496 stage: outcome.fault_stage.as_deref(),
497 system: outcome.fault_system.as_deref(),
498 },
499 );
500 }
501
502 #[cfg(feature = "std")]
503 fn record_panic_fault(&mut self) {
504 self.faulted = true;
505 if self.fault.is_none() {
506 self.fault = Some(AppFault {
507 stage: None,
508 system: None,
509 detail: Some(String::from("panic during execution")),
510 });
511 }
512 let _ = self.world.discard_commands();
513 self.world.set_fixed_step(None);
514 self.run_context.fixed_step = None;
515 self.world.end_run();
516 emit(
517 &mut self.observer,
518 DiagnosticEvent::Fault {
519 stage: None,
520 system: None,
521 },
522 );
523 }
524}
525
526impl AppBuilder {
527 pub fn new() -> AppBuilder {
529 Self {
530 world_builder: WorldBuilder::new(),
531 schedule_builder: ScheduleBuilder::standard(),
532 observer: None,
533 }
534 }
535
536 pub fn world_builder(&mut self) -> &mut WorldBuilder {
538 &mut self.world_builder
539 }
540
541 pub fn schedule_builder(&mut self) -> &mut ScheduleBuilder {
543 &mut self.schedule_builder
544 }
545
546 pub fn add_system(&mut self, system: System) -> Result<&mut Self, BuildError> {
548 self.schedule_builder.add_system(system)?;
549 Ok(self)
550 }
551
552 pub fn insert_resource<R: 'static>(&mut self, value: R) -> &mut Self {
554 self.world_builder.insert_resource(value);
555 self
556 }
557
558 pub fn insert_state<S: Eq + 'static>(&mut self, initial: S) -> &mut Self {
560 self.world_builder.insert_state(initial);
561 self
562 }
563
564 pub fn fixed(&mut self, config: FixedConfig) -> &mut Self {
566 self.schedule_builder.fixed(config);
567 self
568 }
569
570 pub fn set_stage_flush_mode(
572 &mut self,
573 label: impl AsRef<str>,
574 mode: FlushMode,
575 ) -> Result<&mut Self, BuildError> {
576 self.schedule_builder.set_stage_flush_mode(label, mode)?;
577 Ok(self)
578 }
579
580 pub fn observer(&mut self, observer: impl Observer + 'static) -> &mut Self {
582 self.observer = Some(Box::new(observer));
583 self
584 }
585
586 pub fn build(self) -> Result<App, BuildError> {
588 let mut world = self.world_builder.build()?;
589 let schedule = self.schedule_builder.build(&mut world)?;
590 let mut app = App::from_parts(world, schedule)?;
591 app.observer = self.observer;
592 Ok(app)
593 }
594
595 pub fn register_set(
597 &mut self,
598 set: crate::schedule::SystemSet,
599 ) -> Result<&mut Self, BuildError> {
600 self.schedule_builder.register_set(set)?;
601 Ok(self)
602 }
603
604 pub fn set_run_if(
606 &mut self,
607 set: &crate::schedule::SystemSet,
608 condition: crate::schedule::Condition,
609 ) -> Result<&mut Self, BuildError> {
610 self.schedule_builder.set_run_if(set, condition)?;
611 Ok(self)
612 }
613
614 pub fn order_set_before(
616 &mut self,
617 before: &SystemSet,
618 after: &SystemSet,
619 ) -> Result<&mut Self, BuildError> {
620 self.schedule_builder.order_set_before(before, after)?;
621 Ok(self)
622 }
623
624 pub fn order_set_after(
626 &mut self,
627 after: &SystemSet,
628 before: &SystemSet,
629 ) -> Result<&mut Self, BuildError> {
630 self.schedule_builder.order_set_after(after, before)?;
631 Ok(self)
632 }
633}
634
635impl Default for AppBuilder {
636 fn default() -> Self {
637 Self::new()
638 }
639}
640
641fn validate_delta(delta_seconds: f32) -> Result<(), AppError> {
642 if delta_seconds.is_sign_negative() || delta_seconds.is_nan() || delta_seconds.is_infinite() {
643 return Err(AppError::InvalidDelta);
644 }
645 Ok(())
646}
647
648fn duration_from_seconds(delta_seconds: f32) -> Result<Duration, AppError> {
649 Duration::try_from_secs_f32(delta_seconds).map_err(|_| AppError::InvalidDelta)
650}
651
652fn seconds_from_duration(duration: Duration) -> f32 {
653 duration.as_secs_f32()
654}
655
656fn emit(observer: &mut Option<Box<dyn Observer>>, event: DiagnosticEvent<'_>) {
657 if let Some(observer) = observer.as_mut() {
658 observer.observe(event);
659 }
660}
661
662enum GuardedRun<T> {
663 Completed(T),
664 #[cfg(feature = "std")]
665 Panicked(alloc::boxed::Box<dyn core::any::Any + Send>),
666}
667
668struct TerminalUnwindGuard<'a> {
669 world: &'a mut World,
670 run_context: &'a mut RunContext,
671 faulted: &'a mut bool,
672 fault: &'a mut Option<AppFault>,
673 operation: StageOperation,
674 armed: bool,
675}
676
677impl Drop for TerminalUnwindGuard<'_> {
678 fn drop(&mut self) {
679 if !self.armed {
680 return;
681 }
682
683 *self.faulted = true;
684 if self.fault.is_none() {
685 *self.fault = Some(AppFault {
686 stage: None,
687 system: None,
688 detail: Some(String::from("panic during execution")),
689 });
690 }
691 self.run_context.fixed_step = None;
692 self.world.set_fixed_step(None);
693 self.world.end_run();
694 let _ = self.world.discard_commands();
695 self.world.clear_frame_events(self.operation);
696 }
697}
698
699fn with_terminal_unwind_cleanup<R>(
700 world: &mut World,
701 run_context: &mut RunContext,
702 faulted: &mut bool,
703 fault: &mut Option<AppFault>,
704 operation: StageOperation,
705 run: impl FnOnce(&mut World, &mut RunContext) -> R,
706) -> R {
707 let mut guard = TerminalUnwindGuard {
708 world,
709 run_context,
710 faulted,
711 fault,
712 operation,
713 armed: true,
714 };
715 let result = run(&mut *guard.world, &mut *guard.run_context);
716 guard.armed = false;
717 result
718}
719
720fn catch_schedule_panic<R>(f: impl FnOnce() -> R) -> GuardedRun<R> {
721 #[cfg(feature = "std")]
722 {
723 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
724 Ok(result) => GuardedRun::Completed(result),
725 Err(payload) => GuardedRun::Panicked(payload),
726 }
727 }
728 #[cfg(not(feature = "std"))]
729 {
730 GuardedRun::Completed(f())
731 }
732}
733
734fn handle_guarded_run<T>(
735 app: &mut App,
736 run: GuardedRun<Result<T, RunOutcome>>,
737) -> Result<T, AppError> {
738 match run {
739 GuardedRun::Completed(Ok(value)) => Ok(value),
740 GuardedRun::Completed(Err(outcome)) => Err(app.fault_from(outcome)),
741 #[cfg(feature = "std")]
742 GuardedRun::Panicked(payload) => {
743 app.world.end_run();
744 app.record_panic_fault();
745 std::panic::resume_unwind(payload);
746 }
747 }
748}
749
750fn handle_guarded_value<T>(_app: &mut App, run: GuardedRun<T>) -> T {
751 match run {
752 GuardedRun::Completed(value) => value,
753 #[cfg(feature = "std")]
754 GuardedRun::Panicked(payload) => {
755 _app.record_panic_fault();
756 std::panic::resume_unwind(payload);
757 }
758 }
759}
760
761#[cfg(feature = "std")]
762impl core::fmt::Display for AppError {
763 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
764 match self {
765 Self::InvalidDelta => f.write_str("invalid delta"),
766 Self::PendingIdleCommands => f.write_str("pending idle commands"),
767 Self::WorldMutationPoisoned => f.write_str("world mutation poisoned"),
768 Self::TerminalFault => f.write_str("terminal app fault"),
769 Self::WorldTickExhausted => f.write_str("world tick exhausted"),
770 Self::FixedStepExhausted => f.write_str("fixed step exhausted"),
771 Self::InvalidUpdatePlan(_) => f.write_str("invalid update plan"),
772 Self::Fault(_) => f.write_str("app execution fault"),
773 }
774 }
775}
776
777#[cfg(feature = "std")]
778impl std::error::Error for AppError {}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783 use crate::component::ComponentOptions;
784 use crate::schedule::{stage, ScheduleBuilder, System};
785 use crate::time::{ChangeTick, FixedConfig};
786 use crate::world::WorldBuilder;
787 use alloc::vec::Vec;
788 use core::time::Duration;
789
790 #[derive(Clone, Copy)]
791 struct PoisonedComponent;
792
793 fn poison_world(world: &mut World) {
794 let entity = world.spawn().expect("entity");
795 world.insert(entity, PoisonedComponent).expect("seed");
796 world.set_change_tick_for_test(ChangeTick::from_raw(u64::MAX - 1));
797 world
798 .insert(entity, PoisonedComponent)
799 .expect("consume last tick");
800 assert!(matches!(
801 world.insert(entity, PoisonedComponent),
802 Err(crate::world::WorldError::ChangeTickExhausted)
803 ));
804 }
805
806 #[test]
807 fn from_parts_rejects_poisoned_world() {
808 let mut builder = WorldBuilder::new();
809 builder
810 .register_component::<PoisonedComponent>(ComponentOptions::sparse())
811 .expect("component");
812 let mut world = builder.build().expect("world");
813 let schedule = ScheduleBuilder::standard()
814 .build(&mut world)
815 .expect("schedule");
816 poison_world(&mut world);
817
818 assert!(matches!(
819 App::from_parts(world, schedule),
820 Err(BuildError::WorldMutationPoisoned)
821 ));
822 }
823
824 #[test]
825 fn update_rejects_poisoned_world() {
826 let mut builder = AppBuilder::new();
827 builder
828 .world_builder()
829 .register_component::<PoisonedComponent>(ComponentOptions::sparse())
830 .expect("component");
831 builder
832 .add_system(System::new("noop", stage::UPDATE, |_world, _dt| {}))
833 .expect("system");
834 let mut app = builder.build().expect("app");
835 poison_world(app.world_mut());
836
837 assert!(matches!(
838 app.update(1.0 / 60.0),
839 Err(AppError::WorldMutationPoisoned)
840 ));
841 }
842
843 #[test]
844 fn world_tick_exhaustion_faults_app() {
845 let mut app = AppBuilder::new().build().expect("app");
846 app.world_mut().set_world_tick_for_test(u64::MAX);
847
848 assert!(matches!(
849 app.update(1.0 / 60.0),
850 Err(AppError::WorldTickExhausted)
851 ));
852 assert!(app.is_faulted());
853 assert_eq!(
854 app.fault().and_then(|fault| fault.detail.as_deref()),
855 Some("world tick exhausted")
856 );
857 }
858
859 #[test]
860 fn caught_tick_exhaustion_faults_before_next_system() {
861 use core::sync::atomic::{AtomicU32, Ordering};
862
863 static LATER_RUNS: AtomicU32 = AtomicU32::new(0);
864 LATER_RUNS.store(0, Ordering::SeqCst);
865
866 #[derive(Clone, Copy)]
867 struct Counter;
868
869 let mut builder = AppBuilder::new();
870 builder.insert_resource(Counter);
871 builder
872 .add_system(System::new("poison", stage::UPDATE, |world, _dt| {
873 let _ = world.resource_mut::<Counter>();
874 }))
875 .expect("poison system");
876 builder
877 .add_system(System::new("later", stage::UPDATE, |_world, _dt| {
878 LATER_RUNS.fetch_add(1, Ordering::SeqCst);
879 }))
880 .expect("later system");
881 let mut app = builder.build().expect("app");
882 app.world_mut()
883 .set_change_tick_for_test(ChangeTick::from_raw(u64::MAX));
884
885 assert!(matches!(app.update(0.0), Err(AppError::Fault(_))));
886 assert_eq!(LATER_RUNS.load(Ordering::SeqCst), 0);
887 assert_eq!(
888 app.fault().and_then(|fault| fault.system.as_deref()),
889 Some("poison")
890 );
891 }
892
893 #[test]
894 fn fixed_step_exhaustion_records_fault() {
895 let fixed = FixedConfig::new(Duration::from_millis(16))
896 .expect("fixed")
897 .with_max_substeps(1)
898 .expect("cap");
899 let mut world = WorldBuilder::new().build().expect("world");
900 let mut schedule_builder = ScheduleBuilder::standard();
901 schedule_builder.fixed(fixed);
902 schedule_builder
903 .add_system(System::new("fixed", stage::FIXED_UPDATE, |_world, _dt| {}))
904 .expect("fixed");
905 let mut schedule = schedule_builder.build(&mut world).expect("schedule");
906 schedule
907 .fixed_accumulator_mut()
908 .set_next_index_for_test(u64::MAX);
909 let mut app = App::from_parts(world, schedule).expect("app");
910 assert!(matches!(app.update(1.0), Err(AppError::FixedStepExhausted)));
911 assert!(app.is_faulted());
912 }
913
914 #[test]
915 fn later_faults_preserve_the_first_terminal_fault() {
916 let mut app = AppBuilder::default().build().expect("app");
917 let first = AppFault {
918 stage: Some(String::from("first-stage")),
919 system: Some(String::from("first-system")),
920 detail: Some(String::from("first-detail")),
921 };
922 app.fault = Some(first.clone());
923
924 app.record_exhaustion_fault("later exhaustion");
925 assert_eq!(app.fault(), Some(&first));
926
927 app.record_fault(&RunOutcome {
928 fault_stage: Some(String::from("later-stage")),
929 fault_system: Some(String::from("later-system")),
930 fault_detail: Some(String::from("later-detail")),
931 });
932 assert_eq!(app.fault(), Some(&first));
933 }
934
935 #[test]
936 #[cfg(feature = "std")]
937 fn unwind_cleanup_preserves_an_existing_fault() {
938 let mut world = WorldBuilder::new().build().expect("world");
939 let mut context = RunContext::new();
940 let mut faulted = false;
941 let first = AppFault {
942 stage: Some(String::from("first-stage")),
943 system: Some(String::from("first-system")),
944 detail: Some(String::from("first-detail")),
945 };
946 let mut fault = Some(first.clone());
947
948 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
949 with_terminal_unwind_cleanup(
950 &mut world,
951 &mut context,
952 &mut faulted,
953 &mut fault,
954 StageOperation::Update,
955 |_world, _context| panic!("test panic"),
956 );
957 }));
958
959 assert!(result.is_err());
960 assert!(faulted);
961 assert_eq!(fault, Some(first));
962 assert!(world.run_guard_is_idle());
963 }
964
965 #[test]
966 #[cfg(feature = "std")]
967 fn fixed_system_panic_clears_world_and_run_context_steps() {
968 let fixed = FixedConfig::new(Duration::from_millis(16)).expect("fixed");
969 let mut builder = AppBuilder::new();
970 builder.fixed(fixed);
971 builder
972 .add_system(System::new("panic", stage::FIXED_UPDATE, |world, _dt| {
973 assert!(world.fixed_step().is_some());
974 world
975 .commands()
976 .expect("commands")
977 .spawn()
978 .expect("reserve");
979 panic!("fixed panic");
980 }))
981 .expect("system");
982 let mut app = builder.build().expect("app");
983
984 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
985 let _ = app.update(0.016);
986 }));
987
988 assert!(result.is_err());
989 assert!(app.world.fixed_step().is_none());
990 assert!(app.run_context.fixed_step.is_none());
991 assert!(app.world.run_guard_is_idle());
992 assert!(!app.world.has_pending_commands());
993 }
994
995 #[cfg(feature = "std")]
996 #[test]
997 fn panic_fault_can_be_recorded_directly_without_prior_fault() {
998 let mut app = AppBuilder::default().build().expect("app");
999 app.record_panic_fault();
1000 assert!(app.is_faulted());
1001 assert_eq!(
1002 app.fault().and_then(|fault| fault.detail.as_deref()),
1003 Some("panic during execution")
1004 );
1005 }
1006
1007 #[test]
1008 fn builder_set_order_after_delegates_and_default_constructs() {
1009 let before = SystemSet::new("before");
1010 let after = SystemSet::new("after");
1011 let mut builder = AppBuilder::default();
1012 builder.insert_resource(Vec::<&'static str>::new());
1013 builder.register_set(before.clone()).expect("before set");
1014 builder.register_set(after.clone()).expect("after set");
1015 builder
1016 .add_system(
1017 System::new("after", stage::UPDATE, |world, _dt| {
1018 world
1019 .resource_mut::<Vec<&'static str>>()
1020 .expect("trace access")
1021 .expect("trace resource")
1022 .push("after");
1023 })
1024 .in_set(&after),
1025 )
1026 .expect("after system");
1027 builder
1028 .add_system(
1029 System::new("before", stage::UPDATE, |world, _dt| {
1030 world
1031 .resource_mut::<Vec<&'static str>>()
1032 .expect("trace access")
1033 .expect("trace resource")
1034 .push("before");
1035 })
1036 .in_set(&before),
1037 )
1038 .expect("before system");
1039 builder
1040 .order_set_after(&after, &before)
1041 .expect("order after");
1042
1043 let mut app = builder.build().expect("app");
1044 app.update(0.0).expect("update");
1045 assert_eq!(
1046 app.world()
1047 .resource::<Vec<&'static str>>()
1048 .expect("trace access")
1049 .expect("trace resource")
1050 .as_slice(),
1051 ["before", "after"]
1052 );
1053 }
1054}