1
//! `tor_memtrack::tracker::test`
2

            
3
// @@ begin test lint list maintained by maint/add_warning @@
4
#![allow(clippy::bool_assert_comparison)]
5
#![allow(clippy::clone_on_copy)]
6
#![allow(clippy::dbg_macro)]
7
#![allow(clippy::mixed_attributes_style)]
8
#![allow(clippy::print_stderr)]
9
#![allow(clippy::print_stdout)]
10
#![allow(clippy::single_char_pattern)]
11
#![allow(clippy::unwrap_used)]
12
#![allow(clippy::unchecked_time_subtraction)]
13
#![allow(clippy::useless_vec)]
14
#![allow(clippy::needless_pass_by_value)]
15
#![allow(clippy::string_slice)] // See arti#2571
16
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
17
#![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
18
#![allow(clippy::arithmetic_side_effects)] // don't mind potential panicking ops in tests
19

            
20
use super::*;
21

            
22
use std::collections::BTreeMap;
23
use std::fmt::{Display, Write as _};
24
use std::time::Duration;
25

            
26
use itertools::Itertools;
27
use rand::RngExt;
28
use slotmap_careful::Key;
29
use tracing_test::traced_test;
30

            
31
use tor_basic_utils::RngExt as _;
32
use tor_rtcompat::{CoarseDuration, Runtime};
33
use tor_rtmock::MockRuntime;
34

            
35
//---------- useful utilities ----------
36

            
37
pub(crate) const TEST_DEFAULT_LIMIT: usize = mbytes(20);
38
pub(crate) const TEST_DEFAULT_LOWWATER: usize = mbytes(15);
39

            
40
144
fn secs(s: u64) -> CoarseDuration {
41
144
    Duration::from_secs(s).into()
42
144
}
43

            
44
424
pub(crate) const fn mbytes(mib: usize) -> usize {
45
424
    mib * 1024 * 1024
46
424
}
47

            
48
48
fn mk_config() -> Config {
49
48
    Config::builder()
50
48
        .max(TEST_DEFAULT_LIMIT)
51
48
        .low_water(TEST_DEFAULT_LOWWATER)
52
48
        .build()
53
48
        .unwrap()
54
48
}
55

            
56
48
pub(crate) fn mk_tracker(rt: &impl Runtime) -> Arc<MemoryQuotaTracker> {
57
48
    MemoryQuotaTracker::new(&rt, mk_config()).unwrap()
58
48
}
59

            
60
14
fn test_with_various_mocks<F, Fut>(f: F)
61
14
where
62
14
    F: Fn(tor_rtmock::MockRuntime) -> Fut,
63
14
    Fut: Future<Output = ()>,
64
{
65
28
    MockRuntime::test_with_various(|rt| async {
66
        // Make sure we can talk about times at least 1000s in the past
67
        // TODO maybe this should be a feature of MockRuntime but what value to pick?
68
28
        rt.advance_by(Duration::from_secs(1000)).await;
69
28
        f(rt).await;
70
56
    });
71
14
}
72

            
73
//---------- consistency check (test invariants against outside view) ----------
74

            
75
use consistency::*;
76
mod consistency {
77
    use super::*;
78

            
79
    #[derive(Default)]
80
    pub(super) struct CallerInfoCollector {
81
        g: usize,
82
        acs: BTreeMap<AId, refcount::RawCount>,
83
        pcs: BTreeMap<(AId, PId), (refcount::RawCount, usize)>,
84
        debug_dump: String,
85
    }
86

            
87
    pub(super) trait HasCallerInfo {
88
        fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector);
89
    }
90

            
91
    impl CallerInfoCollector {
92
41768
        pub(super) fn note_account(&mut self, acct: &Account, reclaimed: ReclaimedOrOk) {
93
41768
            let acct = acct.0.as_enabled().unwrap();
94
41768
            writeln!(self.debug_dump, "acct {acct:?} {reclaimed:?}").unwrap();
95
41768
            if acct.aid.is_null() || reclaimed.is_err() {
96
32
                return;
97
41736
            }
98
41736
            let ac = self.acs.entry(*acct.aid).or_default();
99
41736
            *ac += 1;
100
41768
        }
101
41768
        pub(super) fn note_particip(
102
41768
            &mut self,
103
41768
            p: &Participation,
104
41768
            reclaimed: ReclaimedOrOk,
105
41768
            used: usize,
106
41768
        ) {
107
41768
            let p = p.0.as_enabled().unwrap();
108
41768
            writeln!(self.debug_dump, "particip {p:?} {reclaimed:?} {used:?}").unwrap();
109
41768
            if p.pid.is_null() || p.aid.is_null() || reclaimed.is_err() {
110
36
                return;
111
41732
            }
112
41732
            self.note_partn_core(p, used);
113
41768
        }
114
40000
        pub(super) fn note_partn_clone(&mut self, p: &Participation) {
115
40000
            let p = p.0.as_enabled().unwrap();
116
40000
            writeln!(self.debug_dump, "partn {p:?}").unwrap();
117
40000
            if p.pid.is_null() {
118
                return;
119
40000
            }
120
40000
            self.note_partn_core(p, 0);
121
40000
        }
122
81732
        fn note_partn_core(&mut self, p: &ParticipationInner, x_used: usize) {
123
81732
            let pc = self.pcs.entry((p.aid, *p.pid)).or_default();
124
81732
            let used = *p.cache.as_raw() + x_used;
125
81732
            pc.0 += 1;
126
81732
            pc.1 += used;
127
81732
            self.g += used;
128
81732
        }
129
    }
130

            
131
40168
    pub(super) fn check_consistency_general(
132
40168
        trk: &Arc<MemoryQuotaTracker>,
133
40168
        collect_caller_info: impl FnOnce(&mut CallerInfoCollector),
134
40168
    ) {
135
40168
        let state = trk.lock().unwrap().into_enabled().unwrap();
136

            
137
40168
        let (expected, debug_dump) = {
138
40168
            let mut c = CallerInfoCollector::default();
139
40168
            collect_caller_info(&mut c);
140
40168
            ((c.g, c.acs, c.pcs), c.debug_dump)
141
40168
        };
142

            
143
40168
        let got = {
144
40168
            let mut gc = 0;
145
40168
            let mut acs = BTreeMap::new();
146
40168
            let mut pcs = BTreeMap::new();
147
41736
            for (aid, arecord) in state.accounts.iter() {
148
41736
                acs.insert(aid, *arecord.refcount);
149
41736
                for (pid, precord) in arecord.ps.iter() {
150
41732
                    let used = *precord.used.as_raw();
151
41732
                    gc += used;
152
41732
                    pcs.insert((aid, pid), (*precord.refcount, used));
153
41732
                }
154
            }
155

            
156
40168
            (gc, acs, pcs)
157
        };
158

            
159
40168
        assert_eq!(
160
            expected, got,
161
            "\n----- dump (start) -----\n{debug_dump}----- dump (end) -----",
162
        );
163
40168
    }
164
}
165

            
166
//---------- common test participant (state) ----------
167

            
168
#[derive(Debug)]
169
struct PartnState {
170
    partn: Participation,
171
    age: Option<CoarseInstant>,
172
    used: usize,
173
    reclaimed: ReclaimedOrOk,
174
    show: String,
175
}
176

            
177
#[derive(Debug)]
178
struct TestPartn {
179
    state: Mutex<PartnState>,
180
}
181

            
182
impl TestPartn {
183
82440
    fn lock(&self) -> MutexGuard<PartnState> {
184
82440
        self.state.lock().unwrap()
185
82440
    }
186
}
187

            
188
impl From<PartnState> for TestPartn {
189
144
    fn from(state: PartnState) -> TestPartn {
190
144
        TestPartn {
191
144
            state: Mutex::new(state),
192
144
        }
193
144
    }
194
}
195

            
196
impl TestPartn {
197
124
    fn get_oldest(&self) -> Option<CoarseInstant> {
198
124
        self.lock().age
199
124
    }
200
52
    fn reclaim(&self) -> ReclaimFuture {
201
52
        let () = mem::replace(&mut self.lock().reclaimed, Err(())).expect("reclaimed twice!");
202
78
        Box::pin(async { Reclaimed::Collapsing })
203
52
    }
204
240
    fn is_reclaimed(&self) -> Result<(), ()> {
205
240
        self.lock().reclaimed
206
240
    }
207
}
208

            
209
impl IsParticipant for TestPartn {
210
12
    fn get_oldest(&self, _: EnabledToken) -> Option<CoarseInstant> {
211
12
        self.get_oldest()
212
12
    }
213
12
    fn reclaim(self: Arc<Self>, _: EnabledToken) -> ReclaimFuture {
214
12
        (*self.clone()).reclaim()
215
12
    }
216
}
217

            
218
impl PartnState {
219
184
    fn claim(&mut self, qty: usize) -> Result<(), crate::Error> {
220
184
        claim_via(&mut self.partn, &self.show, &mut self.used, qty)
221
184
    }
222

            
223
36
    fn release(&mut self, qty: usize) {
224
36
        release_via(&mut self.partn, &self.show, &mut self.used, qty);
225
36
    }
226
}
227

            
228
20332
fn claim_via(
229
20332
    via: &mut Participation,
230
20332
    show: impl Display,
231
20332
    used: &mut usize,
232
20332
    qty: usize,
233
20332
) -> Result<(), crate::Error> {
234
20332
    eprintln!("{show} claim {qty} {qty:#x}");
235
20332
    via.claim(qty)?;
236
20312
    *used += qty;
237
20312
    Ok(())
238
20332
}
239

            
240
19888
fn release_via(via: &mut Participation, show: impl Display, used: &mut usize, qty: usize) {
241
19888
    eprintln!("{show} release {qty} {qty:#x}");
242
19888
    via.release(qty);
243
19888
    *used -= qty;
244
19888
}
245

            
246
impl HasCallerInfo for PartnState {
247
41768
    fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
248
41768
        collector.note_particip(&self.partn, self.reclaimed, self.used);
249
41768
    }
250
}
251

            
252
//---------- test participant which is directly the accountholder ----------
253

            
254
#[derive(Debug, Deref)]
255
struct UnifiedP {
256
    acct: Account,
257
    #[deref]
258
    state: TestPartn,
259
}
260

            
261
type ReclaimedOrOk = Result<(), ()>;
262

            
263
impl IsParticipant for UnifiedP {
264
112
    fn get_oldest(&self, _: EnabledToken) -> Option<CoarseInstant> {
265
112
        self.state.get_oldest()
266
112
    }
267
40
    fn reclaim(self: Arc<Self>, _: EnabledToken) -> ReclaimFuture {
268
40
        self.state.reclaim()
269
40
    }
270
}
271

            
272
impl UnifiedP {
273
120
    fn new(
274
120
        rt: &impl Runtime,
275
120
        trk: &Arc<MemoryQuotaTracker>,
276
120
        parent: Option<&Account>,
277
120
        age: CoarseDuration,
278
120
        show: impl Display,
279
120
    ) -> Arc<Self> {
280
120
        let acct = trk.new_account(parent).unwrap();
281

            
282
120
        let now = rt.now_coarse();
283

            
284
120
        acct.register_participant_with(now, |partn| {
285
120
            Ok::<_, Void>((
286
120
                Arc::new(UnifiedP {
287
120
                    acct: acct.clone(),
288
120
                    state: PartnState {
289
120
                        partn,
290
120
                        age: Some(now - age),
291
120
                        show: show.to_string(),
292
120
                        used: 0,
293
120
                        reclaimed: Ok(()),
294
120
                    }
295
120
                    .into(),
296
120
                }),
297
120
                (),
298
120
            ))
299
120
        })
300
120
        .unwrap()
301
120
        .void_unwrap()
302
        .0
303
120
    }
304

            
305
144
    async fn settle_check_consistency<'i>(
306
144
        rt: &'i MockRuntime,
307
144
        trk: &'i Arc<MemoryQuotaTracker>,
308
144
        ups: impl IntoIterator<Item = &'i Arc<Self>> + 'i,
309
144
    ) {
310
144
        rt.advance_until_stalled().await;
311

            
312
144
        check_consistency_general(trk, |collector| {
313
1744
            for up in ups {
314
1744
                up.note_consistency_caller_info(collector);
315
1744
            }
316
144
        });
317
144
    }
318
}
319

            
320
impl HasCallerInfo for UnifiedP {
321
41752
    fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
322
41752
        let state = self.lock();
323
41752
        collector.note_account(&self.acct, state.reclaimed);
324
41752
        state.note_consistency_caller_info(collector);
325
41752
    }
326
}
327

            
328
//---------- test cases with unified accountholder/participant ----------
329

            
330
#[traced_test]
331
#[test]
332
2
fn basic() {
333
5
    test_with_various_mocks(|rt| async move {
334
4
        let trk = mk_tracker(&rt);
335

            
336
4
        let ps: Vec<Arc<UnifiedP>> = (0..21)
337
84
            .map(|i| UnifiedP::new(&rt, &trk, None, secs(i), i))
338
4
            .collect();
339

            
340
76
        for p in &ps[0..19] {
341
76
            p.lock().claim(mbytes(1)).unwrap();
342
76
            UnifiedP::settle_check_consistency(&rt, &trk, &ps).await;
343
        }
344

            
345
168
        let count_uncollapsed = || ps.iter().filter(|p| p.is_reclaimed().is_ok()).count();
346

            
347
4
        assert_eq!(count_uncollapsed(), 21);
348

            
349
4
        for p in &ps[20..] {
350
            // check that we are exercising a situation with nonzero cached
351
            // (this is set up by register_participant
352
4
            assert_ne!(p.lock().partn.0.as_enabled().unwrap().cache, Qty(0));
353

            
354
4
            p.lock()
355
4
                .claim(mbytes(1))
356
4
                .expect("allocation rejected, during collapse, but collapse is async");
357
        }
358

            
359
4
        UnifiedP::settle_check_consistency(&rt, &trk, &ps).await;
360

            
361
4
        assert_eq!(count_uncollapsed(), 14);
362

            
363
        // Now we drop everything.  This exercises much of the teardown!
364
8
    });
365
2
}
366

            
367
#[traced_test]
368
#[test]
369
2
fn parent() {
370
5
    test_with_various_mocks(|rt| async move {
371
8
        for ages in [[10, 20], [20, 10]] {
372
8
            eprintln!("ages: {ages:?}");
373
8
            let [parent_age, child_age] = ages.map(secs);
374

            
375
8
            let trk = mk_tracker(&rt);
376

            
377
24
            let mk_p = |parent, age, show| UnifiedP::new(&rt, &trk, parent, age, show);
378

            
379
8
            let parent = mk_p(None, parent_age, "parent");
380
8
            parent.lock().claim(mbytes(7)).unwrap();
381
8
            rt.advance_until_stalled().await;
382
8
            assert!(parent.is_reclaimed().is_ok());
383

            
384
8
            let child = mk_p(Some(&parent.acct), child_age, "child");
385
8
            child.lock().claim(mbytes(7)).unwrap();
386
8
            assert!(parent.is_reclaimed().is_ok());
387
8
            assert!(child.is_reclaimed().is_ok());
388

            
389
8
            let trigger = mk_p(None, secs(0), "trigger");
390
8
            trigger.lock().claim(mbytes(7)).unwrap();
391
8
            assert!(trigger.is_reclaimed().is_ok());
392

            
393
8
            rt.advance_until_stalled().await;
394

            
395
8
            if parent_age > child_age {
396
                // parent is older than child, we're supposed to have reclaimed
397
                // from the parent, causing reclamation of the child.
398
4
                assert!(parent.is_reclaimed().is_err());
399
4
                assert!(child.is_reclaimed().is_err());
400
            } else {
401
                // supposed to have reclaimed from child only
402
4
                assert!(parent.is_reclaimed().is_ok());
403
4
                assert!(child.is_reclaimed().is_err());
404
            }
405
        }
406
8
    });
407
2
}
408

            
409
#[traced_test]
410
#[test]
411
2
fn cache() {
412
5
    test_with_various_mocks(|rt| async move {
413
4
        let seq = [
414
4
            1,
415
4
            1000,
416
4
            *MAX_CACHE - 2000,
417
4
            3000,
418
4
            *MAX_CACHE,
419
4
            *MAX_CACHE - 1,
420
4
            *MAX_CACHE + 1,
421
4
        ];
422

            
423
4
        let trk = mk_tracker(&rt);
424
4
        let p = UnifiedP::new(&rt, &trk, None, secs(0), "p");
425

            
426
28
        for qty in seq {
427
28
            p.lock().claim(qty).unwrap();
428
28
            UnifiedP::settle_check_consistency(&rt, &trk, [&p]).await;
429
        }
430

            
431
28
        for qty in seq {
432
28
            p.lock().release(qty);
433
28
            UnifiedP::settle_check_consistency(&rt, &trk, [&p]).await;
434
        }
435

            
436
4
        let mut p2 = p.lock().partn.clone();
437

            
438
4
        let mut rng = tor_basic_utils::test_rng::Config::Deterministic.into_rng();
439
40000
        for _iter in 0..10_000 {
440
40000
            let qty = rng.gen_range_checked(0..=*MAX_CACHE).unwrap();
441
40000
            let p_use_i = rng.gen_range_checked(1..=3).unwrap();
442
            {
443
40000
                let mut state = p.lock();
444
40000
                let state = &mut *state;
445

            
446
                let mut p_use_buf;
447
40000
                let p_use = match p_use_i {
448
13600
                    1 => &mut state.partn,
449
13164
                    2 => &mut p2,
450
                    3 => {
451
13236
                        p_use_buf = p2.clone();
452
13236
                        &mut p_use_buf
453
                    }
454
                    x => panic!("{}", x),
455
                };
456

            
457
40000
                if rng.random() || qty > state.used {
458
20148
                    claim_via(p_use, p_use_i, &mut state.used, qty).unwrap();
459
20148
                } else {
460
19852
                    release_via(p_use, p_use_i, &mut state.used, qty);
461
19852
                }
462
            }
463

            
464
40000
            rt.advance_until_stalled().await;
465
40000
            check_consistency_general(&trk, |collector| {
466
40000
                p.note_consistency_caller_info(collector);
467
40000
                collector.note_partn_clone(&p2);
468
40000
            });
469
        }
470
8
    });
471
2
}
472

            
473
#[traced_test]
474
#[test]
475
2
fn explicit_destroy() {
476
5
    test_with_various_mocks(|rt| async move {
477
4
        let trk = mk_tracker(&rt);
478

            
479
4
        let p0 = UnifiedP::new(&rt, &trk, None, secs(0), "0");
480
4
        let p1 = p0.clone();
481

            
482
4
        p0.lock().claim(mbytes(1)).unwrap();
483
4
        UnifiedP::settle_check_consistency(&rt, &trk, [&p0]).await;
484

            
485
4
        p1.lock().claim(mbytes(2)).unwrap();
486
4
        UnifiedP::settle_check_consistency(&rt, &trk, [&p0]).await;
487

            
488
4
        p1.lock().partn.clone().destroy_participant();
489

            
490
4
        rt.advance_until_stalled().await;
491
4
        check_consistency_general(&trk, |collector| {
492
4
            collector.note_account(&p0.acct, Ok(()));
493
            // We don't note the participation, since it's dead.
494
4
        });
495

            
496
4
        assert!(p1.lock().claim(mbytes(3)).is_err());
497

            
498
        // Now we drop everything.  This exercises much of the teardown!
499
8
    });
500
2
}
501

            
502
//---------- test client with multiple participants per account ----------
503

            
504
#[derive(Debug)]
505
struct ComplexAH {
506
    acct: Account,
507
    ps: Vec<Arc<TestPartn>>,
508
}
509

            
510
impl HasCallerInfo for ComplexAH {
511
8
    fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
512
8
        let reclaimed = self
513
8
            .ps
514
8
            .iter()
515
20
            .map(|p| p.lock().reclaimed)
516
8
            .dedup()
517
8
            .exactly_one()
518
8
            .unwrap();
519

            
520
8
        collector.note_account(&self.acct, reclaimed);
521
16
        for p in &self.ps {
522
16
            p.lock().note_consistency_caller_info(collector);
523
16
        }
524
8
    }
525
}
526

            
527
impl ComplexAH {
528
20
    fn new(trk: &Arc<MemoryQuotaTracker>) -> Self {
529
20
        ComplexAH {
530
20
            acct: trk.new_account(None).unwrap(),
531
20
            ps: vec![],
532
20
        }
533
20
    }
534

            
535
24
    fn add_p(&mut self, now: CoarseInstant, age: CoarseDuration, show: impl Display) -> usize {
536
24
        let (cp, x) = self
537
24
            .acct
538
24
            .register_participant_with(now, |partn| {
539
24
                Ok::<_, Void>((
540
24
                    Arc::new(TestPartn::from(PartnState {
541
24
                        partn,
542
24
                        age: Some(now - age),
543
24
                        show: show.to_string(),
544
24
                        used: 0,
545
24
                        reclaimed: Ok(()),
546
24
                    })),
547
24
                    42,
548
24
                ))
549
24
            })
550
24
            .unwrap()
551
24
            .void_unwrap();
552

            
553
24
        assert_eq!(x, 42);
554

            
555
24
        let i = self.ps.len();
556
24
        self.ps.push(cp);
557
24
        i
558
24
    }
559
}
560

            
561
#[traced_test]
562
#[test]
563
2
fn complex() {
564
5
    test_with_various_mocks(|rt| async move {
565
4
        let trk = mk_tracker(&rt);
566

            
567
4
        let up = UnifiedP::new(&rt, &trk, None, secs(0), "U");
568
4
        let mut ah = ComplexAH::new(&trk);
569
4
        let now = rt.now_coarse();
570

            
571
8
        for age in [5, 9] {
572
8
            ah.add_p(now, secs(age), age);
573
8
        }
574

            
575
8
        let settle_check_consistency = || async {
576
8
            rt.advance_until_stalled().await;
577

            
578
8
            check_consistency_general(&trk, |collector| {
579
8
                up.note_consistency_caller_info(collector);
580
8
                ah.note_consistency_caller_info(collector);
581
8
            });
582
16
        };
583

            
584
4
        up.lock().claim(mbytes(1)).unwrap();
585
4
        ah.ps[0].lock().claim(mbytes(11)).unwrap();
586

            
587
4
        settle_check_consistency().await;
588

            
589
4
        assert!(up.is_reclaimed().is_ok());
590
8
        for p in &ah.ps {
591
8
            assert!(p.is_reclaimed().is_ok());
592
        }
593

            
594
4
        ah.ps[1].lock().claim(mbytes(11)).unwrap();
595

            
596
4
        settle_check_consistency().await;
597
4
        assert!(up.is_reclaimed().is_ok());
598
8
        for p in &ah.ps {
599
8
            assert!(p.is_reclaimed().is_err());
600
        }
601
8
    });
602
2
}
603

            
604
//---------- various error cases ----------
605

            
606
#[derive(Debug)]
607
struct DummyParticipant;
608

            
609
impl IsParticipant for DummyParticipant {
610
    fn get_oldest(&self, _: EnabledToken) -> Option<CoarseInstant> {
611
        None
612
    }
613
    fn reclaim(self: Arc<Self>, _: EnabledToken) -> ReclaimFuture {
614
        Box::pin(async { Reclaimed::Collapsing })
615
    }
616
}
617

            
618
#[traced_test]
619
#[test]
620
2
fn errors() {
621
5
    test_with_various_mocks(|rt| async move {
622
4
        let trk = mk_tracker(&rt);
623
4
        let now = rt.now_coarse();
624

            
625
16
        let mk_ah = || {
626
16
            let mut ah = ComplexAH::new(&trk);
627
16
            ah.add_p(now, secs(5), "p");
628
16
            ah
629
16
        };
630

            
631
        const CLAIM: usize = MAX_CACHE.as_usize() + 1;
632

            
633
8
        let dummy_dangling = || {
634
8
            let p = Arc::new(DummyParticipant);
635
8
            Arc::downgrade(&p)
636
            // p dropped here
637
8
        };
638
4
        assert!(dummy_dangling().upgrade().is_none());
639

            
640
        macro_rules! assert_error { { $error:ident, $r:expr } => {
641
            let r = $r;
642
            assert!(matches!(r, Err(Error::$error)), "unexpected: {:?} => {:?}", stringify!($r), &r);
643
        } }
644

            
645
        // Dropped account
646
        {
647
4
            let mut ah = mk_ah();
648
4
            let wa1: WeakAccount = ah.acct.downgrade();
649
4
            let p = ah.ps.pop().unwrap();
650
4
            assert!(p.lock().claim(1).is_ok());
651
4
            let wa2: WeakAccount = p.lock().partn.account();
652
4
            drop(ah.acct);
653

            
654
4
            rt.advance_until_stalled().await;
655
4
            check_consistency_general(&trk, |_collector| ());
656

            
657
            // account should be dead now
658
4
            assert_error!(AccountClosed, p.lock().claim(CLAIM));
659
4
            assert_error!(AccountClosed, wa1.upgrade());
660
4
            assert_error!(AccountClosed, wa2.upgrade());
661

            
662
            // but we can still release
663
4
            p.lock().release(1);
664
        }
665

            
666
        // Dropped IsParticipant
667
        {
668
4
            let mut ah = mk_ah();
669
4
            let p = ah.ps.pop().unwrap();
670
4
            let mut state = Arc::into_inner(p).unwrap().state.into_inner().unwrap();
671

            
672
4
            state.claim(mbytes(30)).unwrap(); // will trigger reclaim, which discovers the loss
673

            
674
4
            rt.advance_until_stalled().await;
675
4
            check_consistency_general(&trk, |collector| {
676
4
                let reclaimed = Ok(()); // didn't manage to make the callback!
677
4
                collector.note_account(&ah.acct, reclaimed);
678
4
            });
679

            
680
4
            assert_error!(ParticipantShutdown, state.claim(CLAIM));
681
        }
682

            
683
        // Reclaimed account
684
        {
685
4
            let ah = mk_ah();
686
4
            ah.ps[0].lock().claim(mbytes(30)).unwrap();
687

            
688
4
            rt.advance_until_stalled().await;
689
4
            check_consistency_general(&trk, |_collector| ());
690

            
691
4
            let p = &ah.ps[0];
692

            
693
4
            assert!(p.lock().reclaimed.is_err());
694
4
            assert_error!(AccountClosed, p.lock().claim(CLAIM));
695

            
696
4
            let cloned = ah.acct.clone();
697
4
            assert!(cloned.0.as_enabled().unwrap().aid.is_null());
698
4
            assert_error!(
699
                AccountClosed,
700
4
                ah.acct.register_participant(dummy_dangling())
701
            );
702

            
703
4
            let mut cloned = p.lock().partn.clone();
704
4
            assert!(cloned.0.as_enabled().unwrap().pid.is_null());
705
4
            assert_error!(AccountClosed, cloned.claim(CLAIM));
706

            
707
            // but we can still release
708
4
            p.lock().release(1);
709
        }
710

            
711
        // Dropped tracker
712
        {
713
4
            let mut ah = mk_ah();
714
4
            let p = ah.ps.pop().unwrap();
715
4
            let wa = ah.acct.downgrade();
716
4
            drop(ah.acct);
717
4
            let _: MemoryQuotaTracker = Arc::into_inner(trk).unwrap();
718

            
719
4
            assert_error!(TrackerShutdown, wa.upgrade());
720
4
            assert_error!(TrackerShutdown, p.lock().partn.account().upgrade());
721
4
            assert_error!(TrackerShutdown, p.lock().claim(CLAIM));
722
        }
723
8
    });
724
2
}
725

            
726
#[traced_test]
727
#[test]
728
2
fn multi_parent_acct() {
729
5
    test_with_various_mocks(|rt| async move {
730
4
        let trk = mk_tracker(&rt);
731
4
        let parent1 = trk.new_account(None).unwrap();
732
4
        let parent2 = trk.new_account(None).unwrap();
733
4
        let acct = trk.new_account(Some(&parent1)).unwrap();
734
4
        let child = trk.new_account(Some(&acct)).unwrap();
735

            
736
8
        let assert_bug = |err: crate::Result<()>, msg| {
737
8
            let err = match err.unwrap_err() {
738
8
                Error::Bug(e) => e,
739
                e => panic!("unexpected error {e:?}"),
740
            };
741
8
            assert!(err.to_string().contains(msg));
742
8
        };
743

            
744
        // Cannot add self as parent
745
4
        assert_bug(acct.add_parent(&acct), "circular parent relationship");
746

            
747
        // Or self's children
748
4
        assert_bug(acct.add_parent(&child), "circular parent relationship");
749

            
750
        // But parent2 is a valid parent
751
4
        acct.add_parent(&parent2).unwrap();
752

            
753
        // Adding the same parent again shouldn't work,
754
        // because acct is already a child of parent2
755
4
        let err = acct.add_parent(&parent2).unwrap_err();
756
4
        assert!(matches!(err, Error::ChildAccountAlreadyExists), "{err:?}");
757

            
758
4
        let state = trk.lock().unwrap().into_enabled().unwrap();
759

            
760
16
        let assert_is_child = |acct: &Account, parent: &Account, is_child: bool| {
761
16
            let acct = acct.0.as_enabled().unwrap();
762
16
            let parent = parent.0.as_enabled().unwrap();
763
16
            let p_arecord = state
764
16
                .accounts
765
16
                .iter()
766
40
                .find_map(|(aid, arecord)| {
767
40
                    if *parent.aid == aid {
768
16
                        Some(arecord)
769
                    } else {
770
24
                        None
771
                    }
772
40
                })
773
16
                .unwrap();
774

            
775
16
            assert_eq!(p_arecord.children.contains(&acct.aid), is_child);
776
16
        };
777

            
778
4
        assert_is_child(&acct, &parent1, true);
779
4
        assert_is_child(&acct, &parent2, true);
780
4
        assert_is_child(&acct, &acct, false);
781
4
        assert_is_child(&acct, &child, false);
782
8
    });
783
2
}