wowlab_engine_application/
error.rs1use std::fmt;
2
3use wowlab_common::sim::intent::IntentConfigError;
4use wowlab_engine_ports::{EngineError, ResolverError};
5
6use crate::EncounterResolutionError;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10#[non_exhaustive]
11pub enum ApplicationStage {
12 AssistedRotationResolution,
13 ChunkExecution,
14 ConfigParsing,
15 ConflictCheck,
16 EncounterResolution,
17 GameDataResolution,
18 GearResolution,
19 HandlerConstruction,
20 ParallelSimulation,
21 PaperdollResolution,
22 RotationValidation,
23 RotationResolution,
24 SettingsResolution,
25 Simulation,
26 SpecResolution,
27 SpecIntrospection,
28 TalentResolution,
29 WeaponEnchantResolution,
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34#[non_exhaustive]
35pub enum ApplicationErrorCategory {
36 EncounterResolution,
37 Engine,
38 IntentConfig,
39 Resolver,
40}
41
42wowlab_engine_macros::define_error! {
43#[non_exhaustive]
45pub struct ApplicationError {
46 stage: ApplicationStage,
47 #[source]
48 kind: ApplicationErrorKind,
49}
50
51#[derive(Debug, thiserror::Error)]
52enum ApplicationErrorKind {
53 #[error("spec construction error: {0}")]
54 EncounterResolution(#[source] EncounterResolutionError),
55 #[error("{0}")]
56 Engine(#[source] EngineError),
57 #[error("intent validation error: failed to parse sim config TOML: {0}")]
58 IntentConfig(#[source] IntentConfigError),
59 #[error("spec construction error: {context}: {source}")]
60 SpecConstructionResolver {
61 context: &'static str,
62 #[source]
63 source: ResolverError,
64 },
65}
66}
67
68impl ApplicationError {
69 #[must_use]
71 pub fn from_engine(stage: ApplicationStage, source: EngineError) -> Self {
72 Self {
73 stage,
74 kind: ApplicationErrorKind::Engine(source),
75 }
76 }
77
78 pub(crate) fn encounter_resolution(
79 stage: ApplicationStage,
80 source: EncounterResolutionError,
81 ) -> Self {
82 Self {
83 stage,
84 kind: ApplicationErrorKind::EncounterResolution(source),
85 }
86 }
87
88 pub(crate) fn intent_config(stage: ApplicationStage, source: IntentConfigError) -> Self {
89 Self {
90 stage,
91 kind: ApplicationErrorKind::IntentConfig(source),
92 }
93 }
94
95 pub(crate) fn spec_construction_resolver(
96 stage: ApplicationStage,
97 context: &'static str,
98 source: ResolverError,
99 ) -> Self {
100 Self {
101 stage,
102 kind: ApplicationErrorKind::SpecConstructionResolver { context, source },
103 }
104 }
105
106 #[must_use]
107 pub const fn stage(&self) -> ApplicationStage {
108 self.stage
109 }
110
111 #[must_use]
112 pub const fn category(&self) -> ApplicationErrorCategory {
113 match self.kind {
114 ApplicationErrorKind::EncounterResolution(_) => {
115 ApplicationErrorCategory::EncounterResolution
116 }
117 ApplicationErrorKind::Engine(_) => ApplicationErrorCategory::Engine,
118 ApplicationErrorKind::IntentConfig(_) => ApplicationErrorCategory::IntentConfig,
119 ApplicationErrorKind::SpecConstructionResolver { .. } => {
120 ApplicationErrorCategory::Resolver
121 }
122 }
123 }
124
125 #[must_use]
126 pub const fn encounter_resolution_error(&self) -> Option<&EncounterResolutionError> {
127 match &self.kind {
128 ApplicationErrorKind::EncounterResolution(source) => Some(source),
129 _ => None,
130 }
131 }
132
133 #[must_use]
134 pub const fn resolver_error(&self) -> Option<&ResolverError> {
135 match &self.kind {
136 ApplicationErrorKind::SpecConstructionResolver { source, .. } => Some(source),
137 _ => None,
138 }
139 }
140
141 #[cfg(test)]
142 #[must_use]
143 pub(crate) const fn engine_error(&self) -> Option<&EngineError> {
144 match &self.kind {
145 ApplicationErrorKind::Engine(source) => Some(source),
146 _ => None,
147 }
148 }
149
150 #[cfg(test)]
151 #[must_use]
152 pub(crate) const fn intent_config_error(&self) -> Option<&IntentConfigError> {
153 match &self.kind {
154 ApplicationErrorKind::IntentConfig(source) => Some(source),
155 _ => None,
156 }
157 }
158}
159
160impl fmt::Debug for ApplicationError {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 f.debug_struct("ApplicationError")
163 .field("stage", &self.stage())
164 .field("category", &self.category())
165 .finish_non_exhaustive()
166 }
167}
168
169pub(crate) trait EngineResultExt<T> {
170 fn in_application_stage(self, stage: ApplicationStage) -> Result<T, ApplicationError>;
171}
172
173impl<T, E> EngineResultExt<T> for Result<T, E>
174where
175 EngineError: From<E>,
176{
177 fn in_application_stage(self, stage: ApplicationStage) -> Result<T, ApplicationError> {
178 self.map_err(|source| ApplicationError::from_engine(stage, EngineError::from(source)))
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use std::error::Error as _;
185
186 use googletest::prelude::*;
187 use wowlab_engine_ports::{
188 EngineConstructionError, EngineConstructionStage, SimRunError, SpecRuntimeError, SpellId,
189 };
190 use wowlab_types::sim::EnemyIdx;
191
192 use super::*;
193
194 #[gtest]
195 fn invalid_config_retains_common_and_toml_sources_and_exact_presentation() -> Result<()> {
196 let source = crate::settings::parse_config("[").err().or_fail()?;
197 let error = ApplicationError::intent_config(ApplicationStage::ConfigParsing, source);
198
199 verify_that!(error.stage(), eq(ApplicationStage::ConfigParsing))?;
200 verify_that!(error.category(), eq(ApplicationErrorCategory::IntentConfig))?;
201 verify_true!(error.intent_config_error().is_some())?;
202 verify_true!(
203 error
204 .source()
205 .and_then(std::error::Error::source)
206 .is_some_and(<dyn std::error::Error>::is::<IntentConfigError>)
207 )?;
208 verify_true!(
209 error
210 .source()
211 .and_then(std::error::Error::source)
212 .and_then(std::error::Error::source)
213 .and_then(std::error::Error::source)
214 .is_some_and(<dyn std::error::Error>::is::<toml::de::Error>)
215 )?;
216 verify_true!(error.to_string().starts_with(
217 "intent validation error: failed to parse sim config TOML: TOML parse error:"
218 ))?;
219
220 Ok(())
221 }
222
223 #[gtest]
224 fn resolver_context_retains_typed_source_and_exact_presentation() -> Result<()> {
225 let error = ApplicationError::spec_construction_resolver(
226 ApplicationStage::PaperdollResolution,
227 "failed to resolve scaling data",
228 ResolverError::spell_not_found(SpellId::new(42)),
229 );
230
231 verify_that!(error.stage(), eq(ApplicationStage::PaperdollResolution))?;
232 verify_that!(error.category(), eq(ApplicationErrorCategory::Resolver))?;
233 verify_that!(
234 error.to_string(),
235 eq("spec construction error: failed to resolve scaling data: spell 42 not found")
236 )?;
237 verify_true!(error.resolver_error().is_some())?;
238 verify_true!(
239 error
240 .source()
241 .and_then(std::error::Error::source)
242 .is_some_and(<dyn std::error::Error>::is::<ResolverError>)
243 )?;
244
245 Ok(())
246 }
247
248 #[gtest]
249 fn simulation_and_handler_failures_retain_every_typed_source() -> Result<()> {
250 let source = SimRunError::handler(SpecRuntimeError::missing_enemy(EnemyIdx::PRIMARY));
251 let error = ApplicationError::from_engine(
252 ApplicationStage::ChunkExecution,
253 EngineError::from(source),
254 );
255
256 verify_that!(error.category(), eq(ApplicationErrorCategory::Engine))?;
257 verify_that!(error.stage(), eq(ApplicationStage::ChunkExecution))?;
258 let engine = error
259 .source()
260 .and_then(std::error::Error::source)
261 .and_then(|source| source.downcast_ref::<EngineError>())
262 .or_fail()?;
263 let sim = engine
264 .source()
265 .and_then(std::error::Error::source)
266 .and_then(|source| source.downcast_ref::<SimRunError>())
267 .or_fail()?;
268
269 verify_true!(
270 sim.source()
271 .and_then(std::error::Error::source)
272 .is_some_and(<dyn std::error::Error>::is::<SpecRuntimeError>)
273 )?;
274 verify_that!(
275 error.to_string(),
276 eq("simulation runtime error: spec handler failed while processing an event")
277 )?;
278
279 Ok(())
280 }
281
282 #[gtest]
283 fn debug_reports_context_without_leaking_lower_layer_payloads() -> Result<()> {
284 let error = ApplicationError::from_engine(
285 ApplicationStage::HandlerConstruction,
286 EngineError::spec_construction("private resolver credential"),
287 );
288 let debug = format!("{error:?}");
289
290 verify_true!(debug.contains("HandlerConstruction"))?;
291 verify_true!(debug.contains("Engine"))?;
292 verify_true!(!debug.contains("private resolver credential"))?;
293 verify_true!(error.to_string().contains("private resolver credential"))?;
294
295 Ok(())
296 }
297
298 #[gtest]
299 fn construction_failure_retains_ports_and_leaf_sources_without_debug_leakage() -> Result<()> {
300 let error = ApplicationError::from_engine(
301 ApplicationStage::HandlerConstruction,
302 EngineError::rotation_compile(std::io::Error::new(
303 std::io::ErrorKind::InvalidData,
304 "private rotation payload",
305 )),
306 );
307
308 verify_that!(error.stage(), eq(ApplicationStage::HandlerConstruction))?;
309 verify_that!(error.category(), eq(ApplicationErrorCategory::Engine))?;
310 verify_that!(
311 error.to_string(),
312 eq("rotation compile error: private rotation payload")
313 )?;
314 let engine = error
315 .source()
316 .and_then(std::error::Error::source)
317 .and_then(|source| source.downcast_ref::<EngineError>())
318 .or_fail()?;
319 let construction = engine
320 .source()
321 .and_then(std::error::Error::source)
322 .and_then(|source| source.downcast_ref::<EngineConstructionError>())
323 .or_fail()?;
324
325 verify_that!(
326 construction.stage(),
327 eq(EngineConstructionStage::RotationCompile)
328 )?;
329 verify_true!(
330 construction
331 .source()
332 .and_then(std::error::Error::source)
333 .is_some_and(<dyn std::error::Error>::is::<std::io::Error>)
334 )?;
335 let debug = format!("{error:?}");
336
337 verify_true!(debug.contains("HandlerConstruction"))?;
338 verify_true!(!debug.contains("private rotation payload"))?;
339
340 Ok(())
341 }
342}