aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_pool.py
blob: 5c6e2c413abc1e511e54b2af0dfe18ad87ead085 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/env python3
"""Unit tests for idatui.pool (worker residency: LRU + memory budget).

Pure stdlib with a fake client injected, so the eviction policy is testable
without spawning real idalib workers.

    python tests/test_pool.py
"""
import os
import sys
import tempfile

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from idatui.pool import WorkerPool  # noqa: E402
from idatui.project import Project  # noqa: E402

PASS = FAIL = 0


def check(name, cond, detail=""):
    global PASS, FAIL
    if cond:
        PASS += 1
        print(f"  ok   {name}")
    else:
        FAIL += 1
        print(f"  FAIL {name}   {detail}")


class FakeClient:
    """Stands in for a WorkerClient: records saves/closes, reports fixed memory."""

    def __init__(self, ref, mem=100):
        self.ref = ref
        self.mem = mem
        self.saved = 0
        self.closed = False
        self.connected = False

    def connect(self, progress=None, **kw):
        self.connected = True
        return self

    def call(self, tool, **kw):
        if tool == "idb_save":
            self.saved += 1
        return {}

    def close(self, grace=None):
        self.closed = True


def _mkproject(tmp, n=4, size=64):
    src = os.path.join(tmp, "src")
    os.makedirs(src, exist_ok=True)
    paths = []
    for i in range(n):
        p = os.path.join(src, f"bin{i}")
        with open(p, "wb") as f:
            f.write(b"\x7fELF" + bytes(size))
        paths.append(p)
    return Project.create(os.path.join(tmp, "p.json"), paths, name="p")


def main() -> int:
    with tempfile.TemporaryDirectory() as tmp:
        proj = _mkproject(tmp)
        made = {}

        def spawn(ref, ttl):
            c = FakeClient(ref, mem=100)
            made[ref.label] = c
            return c

        pool = WorkerPool(proj, budget_mb=350, spawn=spawn,
                          mem_fn=lambda c: c.mem)

        # -- lazy spawn + reuse -------------------------------------------- #
        a = pool.get("bin0")
        check("get() spawns a worker on first use", a is made["bin0"] and a.connected)
        check("get() stages the binary first",
              os.path.isfile(proj.by_label("bin0").staged))
        check("get() reuses the resident worker", pool.get("bin0") is a)
        check("resident() reports it", pool.resident() == ["bin0"], pool.resident())

        # -- LRU ordering ---------------------------------------------------- #
        pool.get("bin1")
        pool.get("bin2")
        pool.get("bin0")  # touch: bin0 becomes most-recent
        check("LRU order tracks use", pool.resident() == ["bin1", "bin2", "bin0"],
              pool.resident())

        # -- budget eviction -------------------------------------------------- #
        check("pool reports its memory", pool.memory_mb() == 300, pool.memory_mb())
        pool.get("bin3")  # 400MB > 350MB budget -> evict LRU (bin1)
        check("exceeding the budget evicts the least-recently-used",
              pool.evicted == ["bin1"] and not pool.is_resident("bin1"),
              f"evicted={pool.evicted} resident={pool.resident()}")
        check("the just-spawned worker is never the victim", pool.is_resident("bin3"))
        check("eviction saves the database first", made["bin1"].saved == 1)
        check("eviction closes the worker", made["bin1"].closed)
        check("pool is back within budget", pool.memory_mb() <= pool.budget_mb,
              f"{pool.memory_mb()}/{pool.budget_mb}")

        # -- the active binary is never evicted ------------------------------- #
        pool.set_active("bin2")
        check("set_active touches the LRU", pool.resident()[-1] == "bin2",
              pool.resident())
        pool.get("bin1")  # over budget again -> must evict, but not bin2
        check("the active binary survives eviction", pool.is_resident("bin2"),
              f"resident={pool.resident()}")

        # -- pinning ---------------------------------------------------------- #
        pool.close_all()
        pool2 = WorkerPool(proj, budget_mb=250, spawn=spawn, mem_fn=lambda c: c.mem)
        pool2.get("bin0")
        pool2.pin("bin0")
        pool2.get("bin1")
        pool2.get("bin2")  # 300 > 250 -> evict, but bin0 is pinned
        check("pinned binaries are never evicted", pool2.is_resident("bin0"),
              f"resident={pool2.resident()} evicted={pool2.evicted}")
        check("an unpinned one went instead", "bin1" in pool2.evicted, pool2.evicted)

        # -- everything pinned/active: stop evicting rather than thrash -------- #
        pool2.pin("bin2")
        pool2.set_active("bin2")
        n_before = len(pool2.evicted)
        pool2._enforce_budget()
        check("nothing evictable -> gives up instead of thrashing",
              len(pool2.evicted) == n_before, pool2.evicted)

        # -- status for the switcher UI ---------------------------------------- #
        st = {s["label"]: s for s in pool2.status()}
        check("status() covers every project binary", len(st) == 4, list(st))
        check("status() marks resident/pinned/active",
              st["bin0"]["resident"] and st["bin0"]["pinned"]
              and st["bin2"]["active"] and not st["bin3"]["resident"],
              f"{st}")

        # -- teardown ----------------------------------------------------------- #
        pool2.close_all()
        check("close_all() closes every worker",
              not pool2.resident() and all(c.closed for c in made.values()))
        check("close_all() clears the active binary", pool2.active is None)

        # -- unknown label -------------------------------------------------------- #
        try:
            pool2.get("nope")
            check("an unknown label raises KeyError", False, "no raise")
        except KeyError:
            check("an unknown label raises KeyError", True)

        # -- default budget comes from the project's memory_pct ------------------- #
        pool3 = WorkerPool(proj, spawn=spawn, mem_fn=lambda c: c.mem)
        check("default budget is derived, not a fixed worker count",
              pool3.budget_mb >= 256, pool3.budget_mb)

    # -- prewarm: speculative, and never at the cost of a real binary ------ #
    with tempfile.TemporaryDirectory() as tmp:
        proj = _mkproject(tmp, n=3)
        made2 = {}

        def spawn2(ref, ttl):
            c = FakeClient(ref, mem=100)
            made2[ref.label] = c
            return c

        pool = WorkerPool(proj, budget_mb=250, spawn=spawn2,
                          mem_fn=lambda c: c.mem)
        labels = [r.label for r in proj.refs]
        a, b, c_ = labels[0], labels[1], labels[2]
        pool.get(a)
        pool.set_active(a)
        check("prewarm warms a binary when the budget has room",
              pool.prewarm(b) is True and b in pool.resident(), f"{pool.resident()}")
        check("prewarm is a no-op for something already resident",
              pool.prewarm(b) is False)
        # 2 x 100MB resident, estimate 100 more -> 300 > 250: must refuse
        check("prewarm refuses rather than making room",
              pool.prewarm(c_) is False and c_ not in pool.resident(),
              f"resident={pool.resident()} mem={pool.memory_mb()}/{pool.budget_mb}")
        check("refusing to prewarm evicts nothing",
              set(pool.resident()) == {a, b}, f"{pool.resident()}")
        check("prewarm ignores a label outside the project",
              pool.prewarm("nope") is False)

    print(f"\n{PASS} passed, {FAIL} failed")
    return 1 if FAIL else 0


if __name__ == "__main__":
    raise SystemExit(main())