1use crate::consts::{
8 MAX_COMPLETE_SCENARIO_PRODUCT, MAX_ENUM_EXPANSION, MAX_FINGERPRINT_DEPTH, MAX_PARAM_COMBINATIONS,
9 MAX_REQUIRED_PROPERTY_COMBINATIONS, MAX_RESOLVE_DEPTH, MAX_SAT_ITERATIONS, MAX_SCENARIO_COMBINATIONS,
10 MAX_SCHEMA_MATCH_DEPTH, MAX_SCHEMA_SCENARIO_ASSIGNMENTS, MAX_SCHEMA_SCENARIO_MERGE_ATTEMPTS,
11 MAX_TOTAL_SAT_ITERATIONS, MAX_TOTAL_SCENARIO_COMBINATIONS,
12};
13use std::collections::BTreeSet;
14use std::sync::Mutex;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum BudgetKind {
24 ResolverDepth,
26 EnumExpansion,
28 ScenarioCombinationsPerValue,
30 ScenarioProductCovering,
32 ScenarioCombinationsTotal,
34 ConditionParameterCombinations,
36 ConditionSatIterationsPerQuery,
38 ConditionSatIterationsTotal,
40 SchemaScenarioAssignments,
42 SchemaScenarioMergeAttempts,
44 SchemaMatchDepth,
46 RequiredPropertyCombinations,
49 ExpressionFingerprintDepth,
51}
52
53impl BudgetKind {
54 pub const fn limit(self) -> u64 {
56 match self {
57 Self::ResolverDepth => MAX_RESOLVE_DEPTH as u64,
58 Self::EnumExpansion => MAX_ENUM_EXPANSION as u64,
59 Self::ScenarioCombinationsPerValue => MAX_SCENARIO_COMBINATIONS as u64,
60 Self::ScenarioProductCovering => MAX_COMPLETE_SCENARIO_PRODUCT as u64,
61 Self::ScenarioCombinationsTotal => MAX_TOTAL_SCENARIO_COMBINATIONS,
62 Self::ConditionParameterCombinations => MAX_PARAM_COMBINATIONS,
63 Self::ConditionSatIterationsPerQuery => MAX_SAT_ITERATIONS,
64 Self::ConditionSatIterationsTotal => MAX_TOTAL_SAT_ITERATIONS,
65 Self::SchemaScenarioAssignments => MAX_SCHEMA_SCENARIO_ASSIGNMENTS as u64,
66 Self::SchemaScenarioMergeAttempts => MAX_SCHEMA_SCENARIO_MERGE_ATTEMPTS as u64,
67 Self::SchemaMatchDepth => MAX_SCHEMA_MATCH_DEPTH as u64,
68 Self::RequiredPropertyCombinations => MAX_REQUIRED_PROPERTY_COMBINATIONS as u64,
69 Self::ExpressionFingerprintDepth => MAX_FINGERPRINT_DEPTH as u64,
70 }
71 }
72
73 pub const fn analysis_incomplete(self) -> bool {
77 !matches!(self, Self::RequiredPropertyCombinations)
78 }
79
80 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::ResolverDepth => "resolverDepth",
84 Self::EnumExpansion => "enumExpansion",
85 Self::ScenarioCombinationsPerValue => "scenarioCombinationsPerValue",
86 Self::ScenarioProductCovering => "scenarioProductCovering",
87 Self::ScenarioCombinationsTotal => "scenarioCombinationsTotal",
88 Self::ConditionParameterCombinations => "conditionParameterCombinations",
89 Self::ConditionSatIterationsPerQuery => "conditionSatIterationsPerQuery",
90 Self::ConditionSatIterationsTotal => "conditionSatIterationsTotal",
91 Self::SchemaScenarioAssignments => "schemaScenarioAssignments",
92 Self::SchemaScenarioMergeAttempts => "schemaScenarioMergeAttempts",
93 Self::SchemaMatchDepth => "schemaMatchDepth",
94 Self::RequiredPropertyCombinations => "requiredPropertyCombinations",
95 Self::ExpressionFingerprintDepth => "expressionFingerprintDepth",
96 }
97 }
98
99 pub const fn description(self) -> &'static str {
102 match self {
103 Self::ResolverDepth => "Resolving intrinsic functions exceeded the maximum supported nesting depth.",
104 Self::EnumExpansion => "Resolving a value produced more possible values than the supported limit.",
105 Self::ScenarioCombinationsPerValue => {
106 "Resolving a value produced more conditional scenarios than the supported limit."
107 }
108 Self::ScenarioProductCovering => {
109 "Resolving a container replaced its joint conditional scenarios with an alternative-covering set."
110 }
111 Self::ScenarioCombinationsTotal => {
112 "Resolving the template produced more conditional scenarios than the supported cumulative limit."
113 }
114 Self::ConditionParameterCombinations => {
115 "Evaluating template conditions required more parameter combinations than the supported limit."
116 }
117 Self::ConditionSatIterationsPerQuery => {
118 "Evaluating one set of template conditions exceeded the supported work limit."
119 }
120 Self::ConditionSatIterationsTotal => {
121 "Evaluating template conditions exceeded the supported cumulative work limit."
122 }
123 Self::SchemaScenarioAssignments => {
124 "Checking resource schemas required more conditional value assignments than the supported limit."
125 }
126 Self::SchemaScenarioMergeAttempts => {
127 "Checking resource schemas exceeded the supported number of conditional assignment merge attempts."
128 }
129 Self::SchemaMatchDepth => {
130 "Checking resource schemas exceeded the maximum supported recursive matching depth."
131 }
132 Self::RequiredPropertyCombinations => {
133 "Checking required properties omitted some explanation combinations after reaching the supported limit."
134 }
135 Self::ExpressionFingerprintDepth => {
136 "Comparing resolved expressions exceeded the maximum supported nesting depth."
137 }
138 }
139 }
140}
141
142#[derive(Debug, Default)]
146pub(crate) struct BudgetTracker {
147 exhausted: Mutex<BTreeSet<BudgetKind>>,
148}
149
150impl BudgetTracker {
151 pub(crate) fn new() -> Self {
152 Self { exhausted: Mutex::new(BTreeSet::new()) }
153 }
154
155 pub(crate) fn record(&self, kind: BudgetKind) {
157 self.exhausted.lock().unwrap_or_else(|p| p.into_inner()).insert(kind);
158 }
159
160 pub(crate) fn exhausted_kinds(&self) -> BTreeSet<BudgetKind> {
162 self.exhausted.lock().unwrap_or_else(|p| p.into_inner()).clone()
163 }
164
165 pub(crate) fn analysis_incomplete(&self) -> bool {
167 self.exhausted.lock().unwrap_or_else(|p| p.into_inner()).iter().any(|kind| kind.analysis_incomplete())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn record_deduplicates() {
177 let tracker = BudgetTracker::new();
178 tracker.record(BudgetKind::ResolverDepth);
179 tracker.record(BudgetKind::ResolverDepth);
180 assert_eq!(tracker.exhausted_kinds().len(), 1);
181 }
182
183 #[test]
184 fn exhausted_kinds_deterministic_order() {
185 let tracker = BudgetTracker::new();
186 tracker.record(BudgetKind::ExpressionFingerprintDepth);
187 tracker.record(BudgetKind::ResolverDepth);
188 tracker.record(BudgetKind::EnumExpansion);
189 let kinds: Vec<_> = tracker.exhausted_kinds().into_iter().collect();
190 assert_eq!(kinds[0], BudgetKind::ResolverDepth);
191 assert_eq!(kinds[1], BudgetKind::EnumExpansion);
192 assert_eq!(kinds[2], BudgetKind::ExpressionFingerprintDepth);
193 }
194
195 #[test]
196 fn analysis_incomplete_reflects_kind() {
197 let tracker = BudgetTracker::new();
198 tracker.record(BudgetKind::RequiredPropertyCombinations);
199 assert!(!tracker.analysis_incomplete());
200
201 tracker.record(BudgetKind::ResolverDepth);
202 assert!(tracker.analysis_incomplete());
203 }
204
205 #[test]
206 fn limit_values_match_canonical_constants() {
207 use crate::consts::*;
208 assert_eq!(BudgetKind::ResolverDepth.limit(), MAX_RESOLVE_DEPTH as u64);
209 assert_eq!(BudgetKind::EnumExpansion.limit(), MAX_ENUM_EXPANSION as u64);
210 assert_eq!(BudgetKind::ScenarioCombinationsPerValue.limit(), MAX_SCENARIO_COMBINATIONS as u64);
211 assert_eq!(BudgetKind::ScenarioCombinationsTotal.limit(), MAX_TOTAL_SCENARIO_COMBINATIONS);
212 assert_eq!(BudgetKind::ConditionParameterCombinations.limit(), MAX_PARAM_COMBINATIONS);
213 assert_eq!(BudgetKind::ConditionSatIterationsPerQuery.limit(), MAX_SAT_ITERATIONS);
214 assert_eq!(BudgetKind::ConditionSatIterationsTotal.limit(), MAX_TOTAL_SAT_ITERATIONS);
215 assert_eq!(BudgetKind::SchemaScenarioAssignments.limit(), MAX_SCHEMA_SCENARIO_ASSIGNMENTS as u64);
216 assert_eq!(BudgetKind::SchemaScenarioMergeAttempts.limit(), MAX_SCHEMA_SCENARIO_MERGE_ATTEMPTS as u64);
217 assert_eq!(BudgetKind::SchemaMatchDepth.limit(), MAX_SCHEMA_MATCH_DEPTH as u64);
218 assert_eq!(BudgetKind::RequiredPropertyCombinations.limit(), MAX_REQUIRED_PROPERTY_COMBINATIONS as u64);
219 assert_eq!(BudgetKind::ExpressionFingerprintDepth.limit(), MAX_FINGERPRINT_DEPTH as u64);
220 }
221
222 #[test]
223 fn as_str_lower_camel_case() {
224 assert_eq!(BudgetKind::ResolverDepth.as_str(), "resolverDepth");
225 assert_eq!(BudgetKind::EnumExpansion.as_str(), "enumExpansion");
226 assert_eq!(BudgetKind::ScenarioCombinationsPerValue.as_str(), "scenarioCombinationsPerValue");
227 assert_eq!(BudgetKind::ConditionSatIterationsPerQuery.as_str(), "conditionSatIterationsPerQuery");
228 assert_eq!(BudgetKind::SchemaMatchDepth.as_str(), "schemaMatchDepth");
229 assert_eq!(BudgetKind::RequiredPropertyCombinations.as_str(), "requiredPropertyCombinations");
230 assert_eq!(BudgetKind::ExpressionFingerprintDepth.as_str(), "expressionFingerprintDepth");
231 }
232
233 #[test]
234 fn descriptions_are_human_readable_sentences() {
235 let descriptions = [
236 BudgetKind::ResolverDepth.description(),
237 BudgetKind::EnumExpansion.description(),
238 BudgetKind::ScenarioCombinationsPerValue.description(),
239 BudgetKind::ScenarioCombinationsTotal.description(),
240 BudgetKind::ConditionParameterCombinations.description(),
241 BudgetKind::ConditionSatIterationsPerQuery.description(),
242 BudgetKind::ConditionSatIterationsTotal.description(),
243 BudgetKind::SchemaScenarioAssignments.description(),
244 BudgetKind::SchemaScenarioMergeAttempts.description(),
245 BudgetKind::SchemaMatchDepth.description(),
246 BudgetKind::RequiredPropertyCombinations.description(),
247 BudgetKind::ExpressionFingerprintDepth.description(),
248 ];
249
250 assert!(descriptions.iter().all(|description| description.ends_with('.')));
251 assert_eq!(
252 BudgetKind::ConditionParameterCombinations.description(),
253 "Evaluating template conditions required more parameter combinations than the supported limit."
254 );
255 assert_eq!(
256 BudgetKind::RequiredPropertyCombinations.description(),
257 "Checking required properties omitted some explanation combinations after reaching the supported limit."
258 );
259 }
260}