1use std::collections::BTreeSet;
2
3use trellis_core::{Graph, ResourceCommandTrace, ResourceKey, Revision, ScopeId};
4
5use crate::{
6 HostStatusClass, HostStatusRecord, ResourceLedger, ResourceLedgerError, ResourceSnapshot,
7 ResourceStatusContext,
8};
9
10impl<C> ResourceLedger<C> {
11 pub fn assert_command_order(
13 &self,
14 expected: &[ResourceCommandTrace],
15 ) -> Result<(), ResourceLedgerError> {
16 if self.command_trace == expected {
17 Ok(())
18 } else {
19 Err(ResourceLedgerError::CommandOrderMismatch {
20 expected: expected.to_vec(),
21 actual: self.command_trace.clone(),
22 })
23 }
24 }
25
26 pub fn assert_all_resources_have_owner(&self) -> Result<(), ResourceLedgerError> {
28 for (key, snapshot) in &self.resources {
29 if snapshot.owners.is_empty() {
30 return Err(ResourceLedgerError::Orphan {
31 key: key.clone(),
32 context: Some(snapshot.command_context()),
33 });
34 }
35 }
36 Ok(())
37 }
38
39 pub fn assert_no_orphan_resources(&self) -> Result<(), ResourceLedgerError> {
41 self.assert_all_resources_have_owner()
42 }
43
44 pub fn assert_graph_has_no_orphan_resources(
46 &self,
47 graph: &Graph<C>,
48 ) -> Result<(), ResourceLedgerError> {
49 if let Some(key) = graph.orphan_resources().into_iter().next() {
50 Err(ResourceLedgerError::Orphan {
51 context: self.context_for_key(&key),
52 key,
53 })
54 } else {
55 Ok(())
56 }
57 }
58
59 pub fn assert_no_duplicate_close(&self) -> Result<(), ResourceLedgerError> {
61 if let Some(context) = self.duplicate_closes.first() {
62 Err(ResourceLedgerError::DuplicateClose {
63 key: context.key.clone(),
64 context: context.clone(),
65 })
66 } else {
67 Ok(())
68 }
69 }
70
71 pub fn assert_no_unexplained_coalescing(&self) -> Result<(), ResourceLedgerError> {
73 if let Some(coalescence) = self.unexplained_coalescences.first() {
74 Err(ResourceLedgerError::UnexplainedCoalescence {
75 key: coalescence.key.clone(),
76 coalescence: coalescence.clone(),
77 })
78 } else {
79 Ok(())
80 }
81 }
82
83 pub fn assert_no_forbidden_opened(&self) -> Result<(), ResourceLedgerError> {
85 if let Some(context) = self.forbidden_opened.first() {
86 Err(ResourceLedgerError::ForbiddenOpen {
87 key: context.key.clone(),
88 context: Some(context.clone()),
89 })
90 } else {
91 Ok(())
92 }
93 }
94
95 pub fn assert_no_wildcard_resource_opened(&self) -> Result<(), ResourceLedgerError> {
97 self.assert_no_forbidden_opened()
98 }
99
100 pub fn assert_resource_not_open(&self, key: &ResourceKey) -> Result<(), ResourceLedgerError> {
102 if self.resources.contains_key(key) {
103 Err(ResourceLedgerError::StillOpen {
104 key: key.clone(),
105 context: self.context_for_key(key),
106 })
107 } else {
108 Ok(())
109 }
110 }
111
112 pub fn assert_closed_scope_owns_no_resources(
114 &self,
115 scope: ScopeId,
116 ) -> Result<(), ResourceLedgerError> {
117 let resources = self
118 .resources
119 .iter()
120 .filter(|(_, snapshot)| snapshot.owners.contains(&scope))
121 .map(|(key, _)| key.clone())
122 .collect::<Vec<_>>();
123 if resources.is_empty() {
124 Ok(())
125 } else {
126 let contexts = resources
127 .iter()
128 .filter_map(|key| self.context_for_key(key))
129 .collect();
130 Err(ResourceLedgerError::ClosedScopeOwnsResources {
131 scope,
132 resources,
133 contexts,
134 })
135 }
136 }
137
138 pub fn assert_resource_opened_once(
140 &self,
141 key: &ResourceKey,
142 ) -> Result<(), ResourceLedgerError> {
143 self.assert_count(key, "open_count", 1, |snapshot| snapshot.open_count)
144 }
145
146 pub fn assert_resource_closed_once(
148 &self,
149 key: &ResourceKey,
150 ) -> Result<(), ResourceLedgerError> {
151 self.assert_count(key, "close_count", 1, |snapshot| snapshot.close_count)
152 }
153
154 pub fn assert_resource_generation(
156 &self,
157 key: &ResourceKey,
158 expected: u64,
159 ) -> Result<(), ResourceLedgerError> {
160 let actual = self
161 .history
162 .get(key)
163 .map_or(0, |snapshot| snapshot.generation);
164 if actual == expected {
165 Ok(())
166 } else {
167 Err(ResourceLedgerError::GenerationMismatch {
168 key: key.clone(),
169 expected,
170 actual,
171 context: self.context_for_key(key),
172 })
173 }
174 }
175
176 pub fn assert_resource_shared_by(
178 &self,
179 key: &ResourceKey,
180 expected: BTreeSet<ScopeId>,
181 ) -> Result<(), ResourceLedgerError> {
182 let actual = self
183 .resources
184 .get(key)
185 .map(|snapshot| snapshot.owners.clone())
186 .unwrap_or_default();
187 if actual == expected {
188 Ok(())
189 } else {
190 Err(ResourceLedgerError::OwnerMismatch {
191 key: key.clone(),
192 expected,
193 actual,
194 context: self.context_for_key(key),
195 })
196 }
197 }
198
199 pub fn assert_status_is_stale(
201 &self,
202 key: &ResourceKey,
203 command_revision: Revision,
204 ) -> Result<(), ResourceLedgerError> {
205 let Some(record) = self.status_records.iter().find(|record| {
206 record.status.resource_key == *key && record.status.command_revision == command_revision
207 }) else {
208 return Err(ResourceLedgerError::MissingStatus {
209 key: key.clone(),
210 command_revision,
211 });
212 };
213 if record.class == HostStatusClass::Stale {
214 Ok(())
215 } else {
216 Err(ResourceLedgerError::StatusClassMismatch {
217 context: status_context(record),
218 expected: HostStatusClass::Stale,
219 })
220 }
221 }
222
223 pub fn assert_status_did_not_resurrect_closed_scope(
225 &self,
226 scope: ScopeId,
227 ) -> Result<(), ResourceLedgerError> {
228 self.assert_closed_scope_owns_no_resources(scope)?;
229 self.assert_no_status_mutated_closed_scope()
230 }
231
232 pub fn assert_no_status_mutated_closed_scope(&self) -> Result<(), ResourceLedgerError> {
234 for record in &self.status_records {
235 if record.class == HostStatusClass::Late
236 && self.scope_owns_resource(record.status.scope, &record.status.resource_key)
237 {
238 return Err(ResourceLedgerError::StatusMutatedClosedScope {
239 scope: record.status.scope,
240 context: status_context(record),
241 });
242 }
243 }
244 Ok(())
245 }
246
247 fn assert_count(
248 &self,
249 key: &ResourceKey,
250 field: &'static str,
251 expected: usize,
252 count: impl FnOnce(&ResourceSnapshot<C>) -> usize,
253 ) -> Result<(), ResourceLedgerError> {
254 let actual = self.history.get(key).map_or(0, count);
255 if actual == expected {
256 Ok(())
257 } else {
258 Err(ResourceLedgerError::CountMismatch {
259 key: key.clone(),
260 field,
261 expected,
262 actual,
263 context: self.context_for_key(key),
264 })
265 }
266 }
267
268 fn scope_owns_resource(&self, scope: ScopeId, key: &ResourceKey) -> bool {
269 self.resources
270 .get(key)
271 .is_some_and(|snapshot| snapshot.owners.contains(&scope))
272 }
273}
274
275fn status_context(record: &HostStatusRecord) -> ResourceStatusContext {
276 ResourceStatusContext {
277 status: record.status.clone(),
278 class: record.class,
279 last_transaction_id: record.last_transaction_id,
280 last_command_revision: record.last_command_revision,
281 }
282}