Tests: add duplicate key-map test, also test multiple configurations
Duplicate key-map items (while harmless in most cases) may cause unexpected behavior or point to mistakes in the key-map, so best avoid these.
This commit is contained in:
parent
16da9c944c
commit
167c4c6962
Notes:
blender-bot
2023-09-13 08:48:34 +02:00
Referenced by commit 716d1b9e29
, Tests: bl_keymap_validate failure wasn't causing tests to fail
|
@ -16,6 +16,7 @@ This catches the following kinds of issues:
|
|||
- Unused keymaps (keymaps which are defined but not used anywhere).
|
||||
- Event values that don't make sense for the event type, e.g.
|
||||
An escape key could have the value "NORTH" instead of "PRESS".
|
||||
- Identical key-map items.
|
||||
|
||||
This works by taking the keymap data (before it's loaded into Blender),
|
||||
then comparing it with that same keymap after exporting and importing.
|
||||
|
@ -29,14 +30,28 @@ NOTE:
|
|||
import types
|
||||
import typing
|
||||
|
||||
import os
|
||||
import contextlib
|
||||
|
||||
import bpy
|
||||
import bpy # type: ignore
|
||||
|
||||
# Useful for diffing the output to see what changed in context.
|
||||
# this writes keymaps into the current directory with `.orig.py` & `.rewrite.py` extensions.
|
||||
WRITE_OUTPUT_DIR = None # "/tmp", defaults to the systems temp directory.
|
||||
WRITE_OUTPUT_DIR = "/tmp/test" # "/tmp", defaults to the systems temp directory.
|
||||
|
||||
# For each preset, test all of these options.
|
||||
# The key is the preset name, containing a sequence of (attribute, value) pairs to test.
|
||||
#
|
||||
# NOTE(@campbellbarton): only add these for preferences which impact multiple keys as exposing all preferences
|
||||
# this way would create too many combinations making the tests take too long to complete.
|
||||
PRESET_PREFS = {
|
||||
"Blender": (
|
||||
(("select_mouse", 'LEFT'), ("use_alt_tool", False)),
|
||||
(("select_mouse", 'LEFT'), ("use_alt_tool", True)),
|
||||
(("select_mouse", 'RIGHT'), ("rmb_action", 'TWEAK')),
|
||||
(("select_mouse", 'RIGHT'), ("rmb_action", 'FALLBACK_TOOL')),
|
||||
),
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Generic Utilities
|
||||
|
@ -153,14 +168,49 @@ def keymap_data_clean(keyconfig_data: typing.List, *, relaxed: bool) -> None:
|
|||
items[i] = item_op, item_event, None
|
||||
|
||||
|
||||
def keyconfig_activate_and_extract_data(filepath: str, *, relaxed: bool) -> typing.List:
|
||||
def keyconfig_config_as_filename_component(values: typing.Sequence[typing.Tuple[str, typing.Any]]):
|
||||
"""
|
||||
Takes a configuration, eg:
|
||||
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')]
|
||||
|
||||
And returns a filename compatible path:
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
if not values:
|
||||
return ""
|
||||
|
||||
return "(" + quote(
|
||||
".".join([
|
||||
"-".join((str(key), str(val)))
|
||||
for key, val in values
|
||||
]),
|
||||
# Needed so forward slashes aren't included in the resulting name.
|
||||
safe="",
|
||||
) + ")"
|
||||
|
||||
|
||||
def keyconfig_activate_and_extract_data(
|
||||
filepath: str,
|
||||
*,
|
||||
relaxed: bool,
|
||||
config: typing.Sequence[typing.Tuple[str, typing.Any]],
|
||||
) -> typing.List:
|
||||
"""
|
||||
Activate the key-map by filepath,
|
||||
return the key-config data (cleaned for comparison).
|
||||
"""
|
||||
import bl_keymap_utils.io
|
||||
import bl_keymap_utils.io # type: ignore
|
||||
|
||||
if config:
|
||||
bpy.ops.preferences.keyconfig_activate(filepath=filepath)
|
||||
km_prefs = bpy.context.window_manager.keyconfigs.active.preferences
|
||||
for attr, value in config:
|
||||
setattr(km_prefs, attr, value)
|
||||
|
||||
with temp_fn_argument_extractor(bl_keymap_utils.io, "keyconfig_init_from_data") as args_collected:
|
||||
bpy.ops.preferences.keyconfig_activate(filepath=filepath)
|
||||
|
||||
# If called multiple times, something strange is happening.
|
||||
assert(len(args_collected) == 1)
|
||||
args, _kw = args_collected[0]
|
||||
|
@ -169,6 +219,29 @@ def keyconfig_activate_and_extract_data(filepath: str, *, relaxed: bool) -> typi
|
|||
return keyconfig_data
|
||||
|
||||
|
||||
def keyconfig_report_duplicates(keyconfig_data: typing.List) -> str:
|
||||
"""
|
||||
Return true if any of the key-maps have duplicate items.
|
||||
|
||||
Duplicate items are reported so they can be resolved.
|
||||
"""
|
||||
error_text = []
|
||||
for km_idname, km_args, km_items_data in keyconfig_data:
|
||||
items = tuple(km_items_data["items"])
|
||||
unique: typing.Dict[str, typing.List[int]] = {}
|
||||
for i, (item_op, item_event, item_prop) in enumerate(items):
|
||||
# Ensure stable order as `repr` will use order of definition.
|
||||
item_event = {key: item_event[key] for key in sorted(item_event.keys())}
|
||||
if item_prop is not None:
|
||||
item_prop = {key: item_prop[key] for key in sorted(item_prop.keys())}
|
||||
item_repr = repr((item_op, item_event, item_prop))
|
||||
unique.setdefault(item_repr, []).append(i)
|
||||
for key, value in unique.items():
|
||||
if len(value) > 1:
|
||||
error_text.append("\"%s\" %r indices %r for item %r" % (km_idname, km_args, value, key))
|
||||
return "\n".join(error_text)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import os
|
||||
import sys
|
||||
|
@ -186,37 +259,61 @@ def main() -> None:
|
|||
for filepath in presets:
|
||||
name_only = os.path.splitext(os.path.basename(filepath))[0]
|
||||
|
||||
print("KeyMap Validate:", name_only, end=" ... ")
|
||||
for config in PRESET_PREFS.get(name_only, ((),)):
|
||||
name_only_with_config = name_only + keyconfig_config_as_filename_component(config)
|
||||
print("KeyMap Validate:", name_only_with_config, end=" ... ")
|
||||
data_orig = keyconfig_activate_and_extract_data(
|
||||
filepath,
|
||||
relaxed=relaxed,
|
||||
config=config,
|
||||
)
|
||||
|
||||
data_orig = keyconfig_activate_and_extract_data(filepath, relaxed=relaxed)
|
||||
with tempfile.TemporaryDirectory() as dir_temp:
|
||||
filepath_temp = os.path.join(
|
||||
dir_temp,
|
||||
name_only_with_config + ".test" + ".py",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as dir_temp:
|
||||
filepath_temp = os.path.join(dir_temp, name_only + ".test.py")
|
||||
bpy.ops.preferences.keyconfig_export(filepath=filepath_temp, all=True)
|
||||
data_reimport = keyconfig_activate_and_extract_data(filepath_temp, relaxed=relaxed)
|
||||
bpy.ops.preferences.keyconfig_export(filepath=filepath_temp, all=True)
|
||||
data_reimport = keyconfig_activate_and_extract_data(
|
||||
filepath_temp,
|
||||
relaxed=relaxed,
|
||||
# No configuration supported when loading exported key-maps.
|
||||
config=(),
|
||||
)
|
||||
|
||||
# Comparing a pretty printed string tends to give more useful
|
||||
# text output compared to the data-structure. Both will work.
|
||||
if (cmp_message := report_humanly_readable_difference(
|
||||
pprint.pformat(data_orig, indent=0, width=120),
|
||||
pprint.pformat(data_reimport, indent=0, width=120),
|
||||
)):
|
||||
print("FAILED!")
|
||||
sys.stdout.write((
|
||||
"Keymap %s has inconsistency on re-importing:\n"
|
||||
" %r"
|
||||
) % (filepath, cmp_message))
|
||||
has_error = True
|
||||
else:
|
||||
print("OK!")
|
||||
# Comparing a pretty printed string tends to give more useful
|
||||
# text output compared to the data-structure. Both will work.
|
||||
if (cmp_message := report_humanly_readable_difference(
|
||||
pprint.pformat(data_orig, indent=0, width=120),
|
||||
pprint.pformat(data_reimport, indent=0, width=120),
|
||||
)):
|
||||
error_text_consistency = "Keymap %s has inconsistency on re-importing." % cmp_message
|
||||
else:
|
||||
error_text_consistency = ""
|
||||
|
||||
if WRITE_OUTPUT_DIR:
|
||||
name_only_temp = os.path.join(WRITE_OUTPUT_DIR, name_only)
|
||||
print("Writing data to:", name_only_temp + ".*.py")
|
||||
with open(name_only_temp + ".orig.py", 'w') as fh:
|
||||
fh.write(pprint.pformat(data_orig, indent=0, width=120))
|
||||
with open(name_only_temp + ".rewrite.py", 'w') as fh:
|
||||
fh.write(pprint.pformat(data_reimport, indent=0, width=120))
|
||||
# Perform an additional sanity check:
|
||||
# That there are no identical key-map items.
|
||||
error_text_duplicates = keyconfig_report_duplicates(data_orig)
|
||||
|
||||
if error_text_consistency or error_text_duplicates:
|
||||
print("FAILED!")
|
||||
print("%r has errors!" % filepath)
|
||||
if error_text_consistency:
|
||||
print(error_text_consistency)
|
||||
if error_text_duplicates:
|
||||
print(error_text_duplicates)
|
||||
else:
|
||||
print("OK!")
|
||||
|
||||
if WRITE_OUTPUT_DIR:
|
||||
os.makedirs(WRITE_OUTPUT_DIR, exist_ok=True)
|
||||
name_only_temp = os.path.join(WRITE_OUTPUT_DIR, name_only_with_config)
|
||||
print("Writing data to:", name_only_temp + ".*.py")
|
||||
with open(name_only_temp + ".orig.py", 'w') as fh:
|
||||
fh.write(pprint.pformat(data_orig, indent=0, width=120))
|
||||
with open(name_only_temp + ".rewrite.py", 'w') as fh:
|
||||
fh.write(pprint.pformat(data_reimport, indent=0, width=120))
|
||||
if has_error:
|
||||
sys.exit(1)
|
||||
|
||||
|
|
Loading…
Reference in New Issue