mirror of
https://github.com/Gathin23/cleanyourtv.git
synced 2026-10-27 19:54:45 +00:00
tools: manage.py set refuses a number outside the key's range
was written, printed and pushed while integer() keeps its own default for a number outside 1..8, so the switch silently did nothing - the same trap as a wrong type, one step further in. number_in_range() reads the bounds out of the Keys table with the value, truncates like Number.intValue() (8.9 is eight columns on the TV, so 8 is what gets written) and refuses NaN and Infinity, which json.loads accepts and intValue() has no answer for.
This commit is contained in:
1 parent
cc24309dd1
commit
e4983fca18
2 files changed
+86
-2
No files matched your search
@@ -98,6 +98,54 @@ class SetValueTest(unittest.TestCase):
|
||||
self.assertIn("columns", refusal)
|
||||
self.assertIsNone(self.written())
|
||||
|
||||
def test_a_number_key_refuses_a_value_outside_the_tables_range(self):
|
||||
# integer() keeps its own default for a number outside min..max, so set must not write one either.
|
||||
for key, value in (("columns", "9"), ("columns", "0"), ("rows_below_dock", "4"),
|
||||
("sky_intensity", "101"), ("clock_wallpaper_dim", "91"), ("dock_opacity", "29")):
|
||||
with self.subTest(key=key, value=value):
|
||||
self.config.unlink(missing_ok=True)
|
||||
refusal = self.set_key(key, value)
|
||||
self.assertIn(key, refusal)
|
||||
self.assertIn("out of range", refusal)
|
||||
self.assertIn(value, refusal)
|
||||
self.assertIsNone(self.written(), "a refused value must not touch the theme")
|
||||
self.push.assert_not_called()
|
||||
|
||||
def test_a_number_key_takes_both_ends_of_its_range(self):
|
||||
# The bounds are read out of the Keys table, so a range that moves there moves here with it.
|
||||
numbers = {k: v for k, v in keys_table_types().items() if manage.declared_kind({"type": v}) == "number"}
|
||||
self.assertGreaterEqual(len(numbers), 10, "the Keys table's number keys were read")
|
||||
for key, cell in sorted(numbers.items()):
|
||||
low, high = manage.declared_range(cell)
|
||||
with self.subTest(key=key, value=low):
|
||||
self.config.unlink(missing_ok=True)
|
||||
self.assertIsNone(self.set_key(key, str(low)))
|
||||
self.assertEqual(low, self.written()[key])
|
||||
with self.subTest(key=key, value=high):
|
||||
self.assertIsNone(self.set_key(key, str(high)))
|
||||
self.assertEqual(high, self.written()[key])
|
||||
|
||||
def test_a_number_key_writes_the_int_the_launcher_would_read(self):
|
||||
# integer() does ((Number) v).intValue(), so 8.9 is eight columns on the TV: write 8, not 8.9.
|
||||
self.assertIsNone(self.set_key("columns", "8.9"))
|
||||
self.assertEqual(8, self.written()["columns"])
|
||||
self.assertIsInstance(self.written()["columns"], int)
|
||||
|
||||
def test_a_number_key_refuses_nan_and_infinity(self):
|
||||
# json.loads accepts these three; intValue() has no answer for them, so the key keeps its default.
|
||||
for text in ("NaN", "Infinity", "-Infinity"):
|
||||
with self.subTest(value=text):
|
||||
self.config.unlink(missing_ok=True)
|
||||
self.assertIn("finite", self.set_key("columns", text))
|
||||
self.assertIsNone(self.written())
|
||||
self.push.assert_not_called()
|
||||
|
||||
def test_a_range_cell_is_read_as_its_two_bounds(self):
|
||||
self.assertEqual((1, 8), manage.declared_range("1 to 8"))
|
||||
self.assertEqual((30, 100), manage.declared_range("30 to 100"))
|
||||
self.assertEqual((None, None), manage.declared_range("bool"))
|
||||
self.assertEqual((None, None), manage.declared_range("1 to 8 if top_menu"))
|
||||
|
||||
def test_an_object_key_refuses_a_word_and_takes_a_json_object(self):
|
||||
refusal = self.set_key("wallpaper_moods", "nope")
|
||||
self.assertIn("wallpaper_moods", refusal)
|
||||
@@ -151,6 +199,10 @@ class SetValueTest(unittest.TestCase):
|
||||
self.assertIn("return v instanceof Boolean ? (Boolean) v : fallback;", config)
|
||||
self.assertIn('return v instanceof String ? ((String) v).trim() : fallback;', config)
|
||||
self.assertIn("if (!(v instanceof Number)) return fallback;", config)
|
||||
# integer() truncates like Number.intValue() and keeps its default outside min..max: the two rules
|
||||
# set's number handling copies.
|
||||
self.assertIn("int i = ((Number) v).intValue();", config)
|
||||
self.assertIn("return i < min || i > max ? fallback : i;", config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+34
-2
@@ -14,9 +14,11 @@ build-and-install.sh, or the steps in addons/README.md; tools/theme.sh for theme
|
||||
install on an installed add-on updates it (pm install -r). set writes the key into a theme folder of your own and
|
||||
pushes it with tools/theme.sh, as themes/README.md describes; it never edits the project's themes. set reads the
|
||||
value the way the key's own type in the Keys table needs - true or false for a bool, a JSON list, object or number
|
||||
where the table says so, the text itself for a string - and refuses one the launcher would ignore.
|
||||
where the table says so, the text itself for a string - and refuses one the launcher would ignore or a number
|
||||
outside the range the table states.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -156,11 +158,39 @@ def declared_kind(item):
|
||||
return "list"
|
||||
if cell == "object":
|
||||
return "object"
|
||||
if re.fullmatch(r"\d+(?:\.\d+)? to \d+(?:\.\d+)?", cell):
|
||||
if declared_range(cell) != (None, None):
|
||||
return "number"
|
||||
return "string" if cell else None
|
||||
|
||||
|
||||
def declared_range(cell):
|
||||
"""The (low, high) of a range cell such as '1 to 8', or (None, None) when the cell is not a range."""
|
||||
m = re.fullmatch(r"(-?\d+) to (-?\d+)", cell or "")
|
||||
return (int(m.group(1)), int(m.group(2))) if m else (None, None)
|
||||
|
||||
|
||||
def number_in_range(key, item, parsed, text):
|
||||
"""An int the launcher's integer() would keep: truncated like Number.intValue(), inside the table's range.
|
||||
|
||||
integer() is `int i = ((Number) v).intValue(); return i < min || i > max ? fallback : i;`, so a float is
|
||||
truncated on the TV and anything outside the range is silently the default. The range comes from the Keys
|
||||
table, so the value set writes is one the launcher reads back. (lunar_utc_offset's floor in Config.java is
|
||||
-1, the "by locale" sentinel the table's Meaning column calls Unset rather than a number to pass.)
|
||||
"""
|
||||
cell = (item.get("type") or "").strip()
|
||||
low, high = declared_range(cell)
|
||||
if low is None or high is None:
|
||||
# declared_kind() only sends a range cell here; a cell whose shape changed must not pass unchecked.
|
||||
sys.exit(f"{key} has no range in the Keys table, so there is nothing to check {text} against")
|
||||
# json.loads accepts NaN and Infinity; int() has no answer for them and integer() keeps the default.
|
||||
if isinstance(parsed, float) and not math.isfinite(parsed):
|
||||
sys.exit(f"{key} takes {cell}, so pass a finite number, not {text}")
|
||||
value = int(parsed)
|
||||
if not low <= value <= high:
|
||||
sys.exit(f"{key} takes {cell}, so {text} is out of range and the launcher would keep the default")
|
||||
return value
|
||||
|
||||
|
||||
def value_as(key, item, text):
|
||||
"""The argument read the way the key's type needs, or a refusal naming what the key takes."""
|
||||
kind = declared_kind(item)
|
||||
@@ -186,6 +216,8 @@ def value_as(key, item, text):
|
||||
if not isinstance(parsed, wanted) or isinstance(parsed, bool):
|
||||
example = {"list": '["a", "b"]', "object": '{"key": "value"}', "number": "6"}[kind]
|
||||
sys.exit(f"{key} takes {item.get('type')}, so pass {example}, not {text}")
|
||||
if kind == "number":
|
||||
return number_in_range(key, item, parsed, text)
|
||||
return parsed
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user