Website: the machine estimator (/gepigeny, /en/sizing), live but unlisted; SIZING.md §5 in three verdicts; R-997 filed
gates / gates (push) Successful in 6m42s
gates / gates (push) Successful in 6m42s
A household ticks apps and sees, for an 8, 16 and 32 GB box: fits well / fits, but tight / does not fit, the totals, and the apps that burst on CPU. One rule in assets/sizing.js, every number from assets/sizing-data.json (built by scripts/build_sizing_web.py from the 2026-10-13 N100 measurement; test: 7 cases, red-proofed). Unlisted: noindex, robots Disallow, not in the sitemap, linked from nowhere — site_gates.py gate 25 enforces it (UNLISTED twin pairs keep every twin check). SIZING.md §5 corrected: the 8 GB line contradicted its own x1.5 rule; guest sizes 4.0/11.4/27.4 GB with the measured 612 MiB MemTotal shortfall. R-993 gains the fact that the installer accepts a host below any minimum.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the machine estimator's data and rule (website/assets/sizing-data.json, sizing.js).
|
||||
|
||||
No JavaScript runtime exists on DooPlex or in CI (python3 + git only), so the rule is checked here in Python, on
|
||||
the SAME data file the page reads, and the JavaScript is held to having no numbers of its own:
|
||||
1. the committed data file is exactly what scripts/build_sizing_web.py builds
|
||||
2. the four cases of SIZING.md §5 give the expected verdicts by the rule (estimate() below mirrors sizing.js)
|
||||
3. the verdict table in SIZING.md §5 says the same as the rule computes
|
||||
4. sizing.js carries the rule's shape and none of its constants (they all come from the data file)
|
||||
Run: python3 -I scripts/test_build_sizing_web.py
|
||||
"""
|
||||
import io, json, os, re, sys, unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
||||
import build_sizing_web as B
|
||||
|
||||
DATA = os.path.join(ROOT, "website", "assets", "sizing-data.json")
|
||||
JS = os.path.join(ROOT, "website", "assets", "sizing.js")
|
||||
SIZING_MD = os.path.join(ROOT, "documentation", "audits", "sizing-2026-10-13", "SIZING.md")
|
||||
|
||||
HOUSEHOLD = ["immich", "jellyfin", "nextcloud", "home-assistant", "vaultwarden", "paperless-ngx"]
|
||||
WORDS = {"well": "fits well", "tight": "fits, but tight", "no": "does not fit"}
|
||||
|
||||
|
||||
def guest_mib(d, gb): # mirrors sizing.js guestMib()
|
||||
r = d["guest_rule"]
|
||||
total = gb * 1024 - r["memtotal_shortfall_mib"]
|
||||
mem = total - r["reserve_mib"]
|
||||
if mem > total - r["max_gap_mib"]:
|
||||
mem = total - r["max_gap_mib"]
|
||||
if mem < r["min_mib"]:
|
||||
mem = r["min_mib"]
|
||||
if mem > total - r["ceiling_gap_mib"]:
|
||||
mem = total - r["ceiling_gap_mib"]
|
||||
return mem
|
||||
|
||||
|
||||
def estimate(d, slugs): # mirrors sizing.js estimate()
|
||||
by = {a["slug"]: a for a in d["apps"]}
|
||||
peak = d["platform_mb"] + sum(by[s]["peak_mb"] for s in slugs if not by[s].get("builtin"))
|
||||
out = []
|
||||
for gb in d["boxes_gb"]:
|
||||
g = guest_mib(d, gb)
|
||||
out.append("well" if peak * d["headroom"] <= g else ("tight" if peak <= g else "no"))
|
||||
return peak, out
|
||||
|
||||
|
||||
class SizingWeb(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.d = json.load(io.open(DATA, encoding="utf-8"))
|
||||
cls.all = [a["slug"] for a in cls.d["apps"] if not a.get("builtin")]
|
||||
|
||||
def test_committed_file_is_a_fresh_build(self):
|
||||
self.assertEqual(io.open(DATA, encoding="utf-8").read(), B.render(B.build()),
|
||||
"sizing-data.json differs from a fresh build — run scripts/build_sizing_web.py")
|
||||
|
||||
def test_offers_the_62_listed_apps_plus_builtin(self):
|
||||
self.assertEqual(len(self.all), 62)
|
||||
self.assertEqual([a["slug"] for a in self.d["apps"] if a.get("builtin")], ["filebrowser"])
|
||||
|
||||
def test_guest_sizes_follow_the_installer(self):
|
||||
self.assertEqual([guest_mib(self.d, gb) for gb in (8, 16, 32)], [4096, 11676, 28060])
|
||||
self.assertEqual(guest_mib(self.d, 4), 4 * 1024 - 612 - 1024) # a 4 GB host: the ceiling, not 4 GB
|
||||
|
||||
def test_the_four_cases(self):
|
||||
cases = {
|
||||
"household": (HOUSEHOLD, ["no", "well", "well"]),
|
||||
"household without immich": ([s for s in HOUSEHOLD if s != "immich"], ["well", "well", "well"]),
|
||||
"immich alone": (["immich"], ["tight", "well", "well"]),
|
||||
"all 62": (self.all, ["no", "no", "tight"]),
|
||||
"nothing": ([], ["well", "well", "well"]),
|
||||
}
|
||||
for name, (sel, want) in cases.items():
|
||||
self.assertEqual(estimate(self.d, sel)[1], want, name)
|
||||
|
||||
def test_sizing_md_table_agrees_with_the_rule(self):
|
||||
with io.open(SIZING_MD, encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
block = re.search(r"<!-- sizing-verdicts:.*?-->(.*?)<!-- /sizing-verdicts -->", s, re.S)
|
||||
self.assertIsNotNone(block, "SIZING.md has no sizing-verdicts table")
|
||||
rows = {}
|
||||
for line in block.group(1).splitlines():
|
||||
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
if len(cells) == 5 and cells[0] not in ("Selection", "---"):
|
||||
rows[cells[0]] = cells
|
||||
sel = {"Household set": HOUSEHOLD, "Household set without Immich": [s for s in HOUSEHOLD if s != "immich"],
|
||||
"Immich alone": ["immich"], "All 62 apps": self.all}
|
||||
self.assertEqual(set(rows), set(sel))
|
||||
for name, slugs in sel.items():
|
||||
peak, verdicts = estimate(self.d, slugs)
|
||||
self.assertEqual(rows[name][1], "%.1f GB" % (peak / 1024), name + " peak")
|
||||
self.assertEqual(rows[name][2:], [WORDS[v] for v in verdicts], name)
|
||||
|
||||
def test_js_has_the_rule_and_none_of_its_numbers(self):
|
||||
js = io.open(JS, encoding="utf-8").read()
|
||||
for need in ("data.platform_mb", "data.headroom", "r.reserve_mib", "r.max_gap_mib", "r.min_mib",
|
||||
"r.ceiling_gap_mib", "r.memtotal_shortfall_mib", "data.boxes_gb",
|
||||
"peak * data.headroom <= guest", "peak <= guest"):
|
||||
self.assertIn(need, js)
|
||||
code = re.sub(r"/\*.*?\*/", "", js, flags=re.S)
|
||||
code = re.sub(r"(?m)//.*$", "", code)
|
||||
code = re.sub(r"'(?:[^'\\]|\\.)*'", "''", code)
|
||||
nums = set(re.findall(r"(?<![\w.])\d+(?:\.\d+)?", code))
|
||||
self.assertTrue(nums <= {"0", "1", "2", "1024"}, "sizing.js holds its own numbers: %s" % sorted(nums - {"0", "1", "2", "1024"}))
|
||||
|
||||
def test_data_has_no_peak_below_typical(self):
|
||||
for a in self.d["apps"]:
|
||||
self.assertGreaterEqual(a["peak_mb"], a["typical_mb"], a["slug"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user