Website: machine estimator v2 — fold-out categories and the memory bar (still unlisted, R-997)
gates / gates (push) Successful in 6m31s
gates / gates (push) Successful in 6m31s
Each category is a native <details>, closed at first, with an 'n selected' count; a preset opens what it ticks. A sticky memory bar (0-64 GB, marks 8/16/24/32/48/64+): solid fill = smallest box memory where the selection fits well, thin mark = where it fits, but tight; one line names the box to buy. minBoxGb() inverts guestMib() by bisection on it, so the rule is still written once. Box sizes 12/24/48/64 added; tests cover seven sizes and the inverse (red-proofed); SIZING.md table gains 24 GB.
This commit is contained in:
@@ -36,6 +36,19 @@ def guest_mib(d, gb): # mirrors sizing.js guestMib()
|
||||
return mem
|
||||
|
||||
|
||||
def min_box_gb(d, need): # mirrors sizing.js minBoxGb(): a bisection on guest_mib, never a 2nd rule
|
||||
lo, hi = 0.0, 1.0
|
||||
while guest_mib(d, hi) < need:
|
||||
hi *= 2
|
||||
while hi - lo > 1 / 1024:
|
||||
mid = (lo + hi) / 2
|
||||
if guest_mib(d, mid) >= need:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid
|
||||
return hi
|
||||
|
||||
|
||||
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"))
|
||||
@@ -46,6 +59,10 @@ def estimate(d, slugs): # mirrors sizing.js estimate()
|
||||
return peak, out
|
||||
|
||||
|
||||
def first(d, verdicts, want): # mirrors recommend_gb / tight_box_gb in sizing.js
|
||||
return next((gb for gb, v in zip(d["boxes_gb"], verdicts) if v == want), None)
|
||||
|
||||
|
||||
class SizingWeb(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -61,19 +78,39 @@ class SizingWeb(unittest.TestCase):
|
||||
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(self.d["boxes_gb"], [8, 12, 16, 24, 32, 48, 64])
|
||||
self.assertEqual([guest_mib(self.d, gb) for gb in self.d["boxes_gb"]],
|
||||
[4096, 7580, 11676, 19868, 28060, 44444, 60828])
|
||||
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):
|
||||
# columns: 8, 12, 16, 24, 32, 48, 64 GB; then the recommended ("fits well") and the smallest tight size
|
||||
W, T, N = "well", "tight", "no"
|
||||
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"]),
|
||||
"household": (HOUSEHOLD, [N, T, W, W, W, W, W], 16, 12),
|
||||
"household without immich": ([s for s in HOUSEHOLD if s != "immich"], [W, W, W, W, W, W, W], 8, None),
|
||||
"immich alone": (["immich"], [T, W, W, W, W, W, W], 12, 8),
|
||||
"all 62": (self.all, [N, N, N, N, T, W, W], 48, 32), # 24 GB: guest 19,868 < peak 21,783
|
||||
"nothing": ([], [W] * 7, 8, None),
|
||||
}
|
||||
for name, (sel, want) in cases.items():
|
||||
self.assertEqual(estimate(self.d, sel)[1], want, name)
|
||||
for name, (sel, want, rec, tight) in cases.items():
|
||||
peak, verdicts = estimate(self.d, sel)
|
||||
self.assertEqual(verdicts, want, name)
|
||||
self.assertEqual(first(self.d, verdicts, "well"), rec, name + " recommended")
|
||||
self.assertEqual(first(self.d, verdicts, "tight"), tight, name + " smallest tight")
|
||||
|
||||
def test_inverse_agrees_with_the_forward_rule(self):
|
||||
cases = [HOUSEHOLD, [s for s in HOUSEHOLD if s != "immich"], ["immich"], self.all, []]
|
||||
for sel in cases:
|
||||
peak, _ = estimate(self.d, sel)
|
||||
for need in (peak * self.d["headroom"], peak):
|
||||
gb = min_box_gb(self.d, need)
|
||||
self.assertGreaterEqual(guest_mib(self.d, gb), need) # it fits at the answer
|
||||
self.assertLess(guest_mib(self.d, gb - 2 / 1024), need) # and not one step smaller
|
||||
# the recommendation is the first listed size at or above the fill
|
||||
rec = next((b for b in self.d["boxes_gb"] if guest_mib(self.d, b) >= need), None)
|
||||
if rec is not None:
|
||||
self.assertGreaterEqual(rec, gb - 1 / 1024)
|
||||
|
||||
def test_sizing_md_table_agrees_with_the_rule(self):
|
||||
with io.open(SIZING_MD, encoding="utf-8") as f:
|
||||
@@ -83,7 +120,9 @@ class SizingWeb(unittest.TestCase):
|
||||
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", "---"):
|
||||
if cells and cells[0] == "Selection":
|
||||
cols = [int(c.split()[0]) for c in cells[2:]]
|
||||
elif len(cells) > 2 and not cells[0].startswith("---"):
|
||||
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}
|
||||
@@ -91,7 +130,9 @@ class SizingWeb(unittest.TestCase):
|
||||
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)
|
||||
by = dict(zip(self.d["boxes_gb"], verdicts))
|
||||
self.assertIn(24, cols, "SIZING.md's table lacks the 24 GB column")
|
||||
self.assertEqual(rows[name][2:], [WORDS[by[c]] for c in cols], name)
|
||||
|
||||
def test_js_has_the_rule_and_none_of_its_numbers(self):
|
||||
js = io.open(JS, encoding="utf-8").read()
|
||||
@@ -103,7 +144,10 @@ class SizingWeb(unittest.TestCase):
|
||||
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"}))
|
||||
allowed = {"0", "1", "2", "100", "1024"} # 100 = percent, 1024 = MiB per GiB; no rule constant
|
||||
self.assertTrue(nums <= allowed, "sizing.js holds its own numbers: %s" % sorted(nums - allowed))
|
||||
for need in ("data.bar_max_gb", "data.bar_marks_gb", "function minBoxGb", "guestMib(data, mid) >= needMib"):
|
||||
self.assertIn(need, js)
|
||||
|
||||
def test_data_has_no_peak_below_typical(self):
|
||||
for a in self.d["apps"]:
|
||||
|
||||
Reference in New Issue
Block a user