1
//! Lives in its own test binary: the assertion counts every thread in the
2
//! process, so a test running beside it in the same binary perturbs the count.
3
//! Cargo runs test targets one at a time, which makes the measurement exact.
4

            
5
use scripting::executor::ScriptExecutor;
6
use scripting::format::{ContextType, EntityType};
7
use scripting::serializer::MemorySerializer;
8

            
9
const TEST_WASM: &[u8] = include_bytes!("../../web/static/wasm/groceries_markup.wasm");
10

            
11
/// The epoch ticker must not outlive the execution that started it.
12
///
13
/// The epoch counter is shared by every execution on the same engine, so a
14
/// ticker that keeps running after its call spends the budget of whatever runs
15
/// next — and executors are reused (the account-script batch path drives one
16
/// across many transactions). A `JoinHandle` does not stop a thread when
17
/// dropped, it detaches it, so the previous `drop(ticker)` left it ticking, and
18
/// every `?` between the spawn and the call skipped even that.
19
///
20
/// This asserts the fix's actual contract — no ticker thread survives the call —
21
/// by counting live threads, rather than trying to catch the downstream symptom.
22
/// A symptom test was tried first and rejected: a stray bump only does damage if
23
/// it lands inside a later execution's call window, and with the fast fixtures
24
/// available here it lands between runs instead, so the test passed with the bug
25
/// deliberately reintroduced. Thread count fails against that same reintroduction.
26
#[test]
27
1
fn epoch_ticker_does_not_outlive_the_execution() {
28
2
    fn live_threads() -> usize {
29
2
        std::fs::read_dir("/proc/self/task")
30
2
            .expect("Linux /proc/self/task — this test is Linux-only, as is CI")
31
2
            .count()
32
2
    }
33

            
34
    // A long budget: with the ticker detached, these threads linger for a full
35
    // minute each, so the count after the loop is unambiguous.
36
1
    let executor = ScriptExecutor::try_new()
37
1
        .expect("baseline engine")
38
1
        .with_epoch_deadline_ticks(60);
39
1
    let mut serializer = MemorySerializer::new();
40
1
    serializer.set_context(ContextType::EntityCreate, EntityType::Transaction);
41
1
    let input = serializer.finalize(4096);
42

            
43
    // One warm-up run first: wasmtime brings up a parallel-compilation pool
44
    // sized to the host's cores on first use, and counting that as a leak would
45
    // make this test a core-count assertion.
46
1
    executor
47
1
        .execute(TEST_WASM, &input, Some(4096))
48
1
        .expect("warm-up execution failed");
49

            
50
1
    let before = live_threads();
51
8
    for _ in 0..8 {
52
8
        executor
53
8
            .execute(TEST_WASM, &input, Some(4096))
54
8
            .expect("execution failed");
55
8
    }
56
1
    let after = live_threads();
57

            
58
1
    assert!(
59
1
        after <= before + 1,
60
        "ticker threads outlived their executions: {before} live threads before 8 runs, \
61
         {after} after — a detached ticker keeps bumping the shared engine epoch"
62
    );
63
1
}