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
72
fn secs(s: u64) -> CoarseDuration {
41
72
    Duration::from_secs(s).into()
42
72
}
43

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

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

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

            
60
7
fn test_with_various_mocks<F, Fut>(f: F)
61
7
where
62
7
    F: Fn(tor_rtmock::MockRuntime) -> Fut,
63
7
    Fut: Future<Output = ()>,
64
{
65
14
    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
14
        rt.advance_by(Duration::from_secs(1000)).await;
69
14
        f(rt).await;
70
28
    });
71
7
}
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
20884
        pub(super) fn note_account(&mut self, acct: &Account, reclaimed: ReclaimedOrOk) {
93
20884
            let acct = acct.0.as_enabled().unwrap();
94
20884
            writeln!(self.debug_dump, "acct {acct:?} {reclaimed:?}").unwrap();
95
20884
            if acct.aid.is_null() || reclaimed.is_err() {
96
16
                return;
97
20868
            }
98
20868
            let ac = self.acs.entry(*acct.aid).or_default();
99
20868
            *ac += 1;
100
20884
        }
101
20884
        pub(super) fn note_particip(
102
20884
            &mut self,
103
20884
            p: &Participation,
104
20884
            reclaimed: ReclaimedOrOk,
105
20884
            used: usize,
106
20884
        ) {
107
20884
            let p = p.0.as_enabled().unwrap();
108
20884
            writeln!(self.debug_dump, "particip {p:?} {reclaimed:?} {used:?}").unwrap();
109
20884
            if p.pid.is_null() || p.aid.is_null() || reclaimed.is_err() {
110
18
                return;
111
20866
            }
112
20866
            self.note_partn_core(p, used);
113
20884
        }
114
20000
        pub(super) fn note_partn_clone(&mut self, p: &Participation) {
115
20000
            let p = p.0.as_enabled().unwrap();
116
20000
            writeln!(self.debug_dump, "partn {p:?}").unwrap();
117
20000
            if p.pid.is_null() {
118
                return;
119
20000
            }
120
20000
            self.note_partn_core(p, 0);
121
20000
        }
122
40866
        fn note_partn_core(&mut self, p: &ParticipationInner, x_used: usize) {
123
40866
            let pc = self.pcs.entry((p.aid, *p.pid)).or_default();
124
40866
            let used = *p.cache.as_raw() + x_used;
125
40866
            pc.0 += 1;
126
40866
            pc.1 += used;
127
40866
            self.g += used;
128
40866
        }
129
    }
130

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

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

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

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

            
159
20084
        assert_eq!(
160
            expected, got,
161
            "\n----- dump (start) -----\n{debug_dump}----- dump (end) -----",
162
        );
163
20084
    }
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
41220
    fn lock(&self) -> MutexGuard<PartnState> {
184
41220
        self.state.lock().unwrap()
185
41220
    }
186
}
187

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

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

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

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

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

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

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

            
246
impl HasCallerInfo for PartnState {
247
20884
    fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
248
20884
        collector.note_particip(&self.partn, self.reclaimed, self.used);
249
20884
    }
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
56
    fn get_oldest(&self, _: EnabledToken) -> Option<CoarseInstant> {
265
56
        self.state.get_oldest()
266
56
    }
267
20
    fn reclaim(self: Arc<Self>, _: EnabledToken) -> ReclaimFuture {
268
20
        self.state.reclaim()
269
20
    }
270
}
271

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

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

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

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

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

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

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

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

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

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

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

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

            
349
2
        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
2
            assert_ne!(p.lock().partn.0.as_enabled().unwrap().cache, Qty(0));
353

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

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

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

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

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

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

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

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

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

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

            
393
4
            rt.advance_until_stalled().await;
394

            
395
4
            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
2
                assert!(parent.is_reclaimed().is_err());
399
2
                assert!(child.is_reclaimed().is_err());
400
            } else {
401
                // supposed to have reclaimed from child only
402
2
                assert!(parent.is_reclaimed().is_ok());
403
2
                assert!(child.is_reclaimed().is_err());
404
            }
405
        }
406
4
    });
407
1
}
408

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
498
        // Now we drop everything.  This exercises much of the teardown!
499
4
    });
500
1
}
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
4
    fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
512
4
        let reclaimed = self
513
4
            .ps
514
4
            .iter()
515
8
            .map(|p| p.lock().reclaimed)
516
4
            .dedup()
517
4
            .exactly_one()
518
4
            .unwrap();
519

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

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

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

            
553
12
        assert_eq!(x, 42);
554

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

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

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

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

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

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

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

            
587
2
        settle_check_consistency().await;
588

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

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

            
596
2
        settle_check_consistency().await;
597
2
        assert!(up.is_reclaimed().is_ok());
598
4
        for p in &ah.ps {
599
4
            assert!(p.is_reclaimed().is_err());
600
        }
601
4
    });
602
1
}
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
1
fn errors() {
621
2
    test_with_various_mocks(|rt| async move {
622
2
        let trk = mk_tracker(&rt);
623
2
        let now = rt.now_coarse();
624

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

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

            
633
4
        let dummy_dangling = || {
634
4
            let p = Arc::new(DummyParticipant);
635
4
            Arc::downgrade(&p)
636
            // p dropped here
637
4
        };
638
2
        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
2
            let mut ah = mk_ah();
648
2
            let wa1: WeakAccount = ah.acct.downgrade();
649
2
            let p = ah.ps.pop().unwrap();
650
2
            assert!(p.lock().claim(1).is_ok());
651
2
            let wa2: WeakAccount = p.lock().partn.account();
652
2
            drop(ah.acct);
653

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
750
        // But parent2 is a valid parent
751
2
        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
2
        let err = acct.add_parent(&parent2).unwrap_err();
756
2
        assert!(matches!(err, Error::ChildAccountAlreadyExists), "{err:?}");
757

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

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

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

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