fix(validator): validate all plugin setting types
This commit is contained in:
@@ -307,6 +307,73 @@ Configure the update interval in plugin settings.
|
||||
self.assertTrue(any("## Settings" in error for error in self.validate_readme(without_settings)))
|
||||
|
||||
|
||||
class SettingTypeTests(unittest.TestCase):
|
||||
TRANSLATIONS = {"settings": {"value": {"label": "Value"}}}
|
||||
|
||||
def validate_setting(self, setting: dict, plugin_api: object = 6) -> list[str]:
|
||||
validator = validate_plugins.Validator(Path("/repo"))
|
||||
validator.validate_settings(
|
||||
Path("/repo/example/plugin.toml"),
|
||||
self.TRANSLATIONS,
|
||||
[{"key": "value", "label_key": "settings.value.label", **setting}],
|
||||
"setting",
|
||||
plugin_api,
|
||||
)
|
||||
return validator.errors
|
||||
|
||||
def test_setting_type_catalog_matches_shell_schema(self) -> None:
|
||||
self.assertEqual(
|
||||
validate_plugins.SETTING_TYPES,
|
||||
{
|
||||
"string",
|
||||
"string_list",
|
||||
"string_map",
|
||||
"bool",
|
||||
"int",
|
||||
"double",
|
||||
"select",
|
||||
"file",
|
||||
"folder",
|
||||
"glyph",
|
||||
"color",
|
||||
},
|
||||
)
|
||||
|
||||
def test_accepts_double_with_numeric_bounds(self) -> None:
|
||||
self.assertEqual(
|
||||
self.validate_setting(
|
||||
{"type": "double", "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_rejects_invalid_double_default(self) -> None:
|
||||
errors = self.validate_setting({"type": "double", "default": "fast"})
|
||||
self.assertTrue(any("default must be a finite number" in error for error in errors))
|
||||
|
||||
def test_rejects_invalid_double_range(self) -> None:
|
||||
errors = self.validate_setting(
|
||||
{"type": "double", "default": 0.5, "min": 1.0, "max": 0.0}
|
||||
)
|
||||
self.assertTrue(any("min must be less than or equal to max" in error for error in errors))
|
||||
|
||||
def test_accepts_string_map(self) -> None:
|
||||
self.assertEqual(
|
||||
self.validate_setting(
|
||||
{"type": "string_map", "default": {"eDP-1": "laptop", "DP-1": "monitor"}}
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_rejects_non_string_map_value(self) -> None:
|
||||
errors = self.validate_setting({"type": "string_map", "default": {"eDP-1": 1}})
|
||||
self.assertTrue(any("default.eDP-1 must be a string" in error for error in errors))
|
||||
|
||||
def test_string_map_requires_plugin_api_6(self) -> None:
|
||||
errors = self.validate_setting({"type": "string_map", "default": {}}, plugin_api=5)
|
||||
self.assertTrue(any("string_map requires plugin_api >= 6" in error for error in errors))
|
||||
|
||||
|
||||
class WidgetActionsTests(unittest.TestCase):
|
||||
def validate_actions(self, entry: dict, plugin_api: object = 14) -> list[str]:
|
||||
validator = validate_plugins.Validator(Path("/repo"))
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
@@ -109,7 +110,19 @@ ROOT_STRING_FIELDS = (
|
||||
ROOT_ARRAY_FIELDS = ("dependencies", "tags")
|
||||
ENTRY_TYPES = ("widget", "panel", "shortcut", "desktop_widget", "launcher_provider", "service")
|
||||
SETTING_OWNER_TYPES = ("widget", "panel", "desktop_widget", "launcher_provider")
|
||||
SETTING_TYPES = {"string", "string_list", "bool", "glyph", "select", "folder", "file", "int", "color"}
|
||||
SETTING_TYPES = {
|
||||
"string",
|
||||
"string_list",
|
||||
"string_map",
|
||||
"bool",
|
||||
"glyph",
|
||||
"select",
|
||||
"folder",
|
||||
"file",
|
||||
"int",
|
||||
"double",
|
||||
"color",
|
||||
}
|
||||
PANEL_PLACEMENTS = {"attached", "floating"}
|
||||
PANEL_KEYBOARD_FOCUS = {"on_demand", "exclusive", "none"}
|
||||
WIDGET_GESTURES = {
|
||||
@@ -171,6 +184,7 @@ SETTING_FIELDS = {
|
||||
"description_key",
|
||||
"default",
|
||||
"options",
|
||||
"extensions",
|
||||
"min",
|
||||
"max",
|
||||
"step",
|
||||
@@ -885,6 +899,7 @@ class Validator:
|
||||
translations,
|
||||
entry["setting"],
|
||||
f"{context}.setting",
|
||||
manifest.get("plugin_api"),
|
||||
)
|
||||
|
||||
if entry_count == 0:
|
||||
@@ -907,8 +922,23 @@ class Validator:
|
||||
return
|
||||
|
||||
default = setting["default"]
|
||||
if setting_type in {"string", "folder", "file"} and not isinstance(default, str):
|
||||
self.add_context_error(manifest_path, context, "default must be a string")
|
||||
if setting_type in {"string", "folder", "file"}:
|
||||
if not isinstance(default, str):
|
||||
self.add_context_error(manifest_path, context, "default must be a string")
|
||||
elif setting_type == "double":
|
||||
if not is_number(default) or not math.isfinite(default):
|
||||
self.add_context_error(manifest_path, context, "default must be a finite number")
|
||||
elif setting_type == "string_map":
|
||||
if not isinstance(default, dict):
|
||||
self.add_context_error(manifest_path, context, "default must be a table of string values")
|
||||
else:
|
||||
for key, value in default.items():
|
||||
if not isinstance(value, str):
|
||||
self.add_context_error(
|
||||
manifest_path,
|
||||
context,
|
||||
f"default.{key} must be a string",
|
||||
)
|
||||
elif setting_type in {"glyph", "color"} and not is_non_empty_string(default):
|
||||
self.add_context_error(manifest_path, context, "default must be a non-empty string")
|
||||
elif setting_type == "string_list":
|
||||
@@ -983,18 +1013,24 @@ class Validator:
|
||||
|
||||
return values
|
||||
|
||||
def validate_int_bounds(self, manifest_path: Path, context: str, setting: dict[str, Any]) -> None:
|
||||
def validate_numeric_bounds(self, manifest_path: Path, context: str, setting: dict[str, Any]) -> None:
|
||||
setting_type = setting.get("type")
|
||||
bound_values: dict[str, int] = {}
|
||||
bound_values: dict[str, int | float] = {}
|
||||
|
||||
for field in ("min", "max", "step"):
|
||||
if field not in setting:
|
||||
continue
|
||||
value = setting[field]
|
||||
if setting_type != "int":
|
||||
self.add_context_error(manifest_path, context, f"{field} is only valid for int settings")
|
||||
elif not is_int(value):
|
||||
if setting_type not in {"int", "double"}:
|
||||
self.add_context_error(
|
||||
manifest_path,
|
||||
context,
|
||||
f"{field} is only valid for int or double settings",
|
||||
)
|
||||
elif setting_type == "int" and not is_int(value):
|
||||
self.add_context_error(manifest_path, context, f"{field} must be an integer")
|
||||
elif setting_type == "double" and (not is_number(value) or not math.isfinite(value)):
|
||||
self.add_context_error(manifest_path, context, f"{field} must be a finite number")
|
||||
else:
|
||||
bound_values[field] = value
|
||||
|
||||
@@ -1005,7 +1041,10 @@ class Validator:
|
||||
self.add_context_error(manifest_path, context, "step must be greater than zero")
|
||||
|
||||
default = setting.get("default")
|
||||
if setting_type == "int" and is_int(default):
|
||||
valid_default = (setting_type == "int" and is_int(default)) or (
|
||||
setting_type == "double" and is_number(default) and math.isfinite(default)
|
||||
)
|
||||
if valid_default:
|
||||
if "min" in bound_values and default < bound_values["min"]:
|
||||
self.add_context_error(manifest_path, context, "default must be greater than or equal to min")
|
||||
if "max" in bound_values and default > bound_values["max"]:
|
||||
@@ -1049,6 +1088,7 @@ class Validator:
|
||||
translations: Any | None,
|
||||
settings: Any,
|
||||
context_prefix: str,
|
||||
plugin_api: Any,
|
||||
) -> None:
|
||||
if not isinstance(settings, list):
|
||||
self.add_context_error(manifest_path, context_prefix, "must be an array of tables")
|
||||
@@ -1100,10 +1140,22 @@ class Validator:
|
||||
option_values = self.validate_options(manifest_path, translations, context, setting)
|
||||
if setting_type in SETTING_TYPES:
|
||||
self.validate_default(manifest_path, context, setting_type, setting, option_values)
|
||||
|
||||
self.validate_int_bounds(manifest_path, context, setting)
|
||||
self.validate_numeric_bounds(manifest_path, context, setting)
|
||||
if setting_type == "string_map" and (not is_int(plugin_api) or plugin_api < 6):
|
||||
self.add_context_error(manifest_path, context, "string_map requires plugin_api >= 6")
|
||||
self.validate_visible_when(manifest_path, context, setting.get("visible_when"))
|
||||
|
||||
if "extensions" in setting:
|
||||
if setting_type != "file":
|
||||
self.add_context_error(manifest_path, context, "extensions is only valid for file settings")
|
||||
self.validate_string_list(
|
||||
manifest_path,
|
||||
context,
|
||||
"extensions",
|
||||
setting["extensions"],
|
||||
allow_empty=True,
|
||||
)
|
||||
|
||||
if "advanced" in setting and not isinstance(setting["advanced"], bool):
|
||||
self.add_context_error(manifest_path, context, "advanced must be a bool")
|
||||
|
||||
@@ -1315,7 +1367,13 @@ class Validator:
|
||||
self.validate_no_symlinks(manifest_path, plugin_dir)
|
||||
|
||||
if "setting" in manifest:
|
||||
self.validate_settings(manifest_path, translations, manifest["setting"], "setting")
|
||||
self.validate_settings(
|
||||
manifest_path,
|
||||
translations,
|
||||
manifest["setting"],
|
||||
"setting",
|
||||
manifest.get("plugin_api"),
|
||||
)
|
||||
|
||||
self.validate_entries(manifest_path, manifest, translations)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user