Fix #6669. bSDD now supports test dictionaries, refactor domain to dictionary, support zero length keyword searches, multi-dict support, and basic tests.

Feature tests can now have more sophisticated per UIList item spies.
This commit is contained in:
Dion Moult
2025-05-17 21:27:47 +10:00
parent 8e34c1d7df
commit 22f10e5549
14 changed files with 420 additions and 183 deletions
+34
View File
@@ -0,0 +1,34 @@
@bsdd
Feature: bSDD
Scenario: Load bSDD dictionaries
Given an empty Blender session
And I look at the "buildingSMART Data Dictionary" panel
When I click "Load bSDD Dictionaries"
Then I see "Selected dictionary"
And I see "LCA" in the "1st" list
Scenario: Add dictionaries as classification systems
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "LCA" in the "1st" list
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I look at the "Classifications" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "All Dictionaries"
And I click "Add Classification From bSDD"
Then I see "LCA"
And I see "BonsaiTestDict"
Scenario: Add a single dictionary as a classification systems
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I look at the "Classifications" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "BonsaiTestDict"
And I click "Add Classification From bSDD"
Then I see "BonsaiTestDict"
And I don't see "LCA"
+162 -13
View File
@@ -50,6 +50,67 @@ variables = {
webbrowser.open = lambda x: True
class bSDDClientStub:
def get_dictionary(self, dictionary_uri=None, include_test_dictionaries=False):
dicts = {
"dictionaries": [
{
"availableLanguages": [{"code": "EN", "name": "English"}],
"code": "LCA",
"uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0",
"name": "LCA indicators and modules",
"version": "3.0",
"organizationCodeOwner": "LCA",
"organizationNameOwner": "buildingSMART Sustainability Strategic Group",
"defaultLanguageCode": "EN",
"isLatestVersion": True,
"isVerified": False,
"isPrivate": False,
"license": "No license (rights reserved)",
"licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/",
"qualityAssuranceProcedure": "EN ISO 23386:2020",
"status": "Active",
"moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/",
"releaseDate": "2023-12-01T14:14:19Z",
"lastUpdatedUtc": "2023-12-01T14:17:53Z",
},
{
"availableLanguages": [{"code": "EN", "name": "English"}],
"code": "BonsaiTestDict",
"uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict",
"name": "BonsaiTestDict",
"version": "3.0",
"organizationCodeOwner": "LCA",
"organizationNameOwner": "buildingSMART Sustainability Strategic Group",
"defaultLanguageCode": "EN",
"isLatestVersion": True,
"isVerified": False,
"isPrivate": False,
"license": "No license (rights reserved)",
"licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/",
"qualityAssuranceProcedure": "EN ISO 23386:2020",
"status": "Active",
"moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/",
"releaseDate": "2023-12-01T14:14:19Z",
"lastUpdatedUtc": "2023-12-01T14:17:53Z",
}
],
"totalCount": 2,
"offset": 0,
"count": 2,
}
if not dictionary_uri:
return dicts
for dictionary in dicts["dictionaries"]:
if dictionary["uri"] == dictionary_uri:
dicts["dictionaries"] = [dictionary]
return dicts
assert False, f"Could not find dictionary uri {dictionary_uri}"
tool.Bsdd.client = bSDDClientStub()
class PanelSpy:
def __init__(self, panel: type[bpy.types.Panel]):
self.is_spy_dirty = True
@@ -86,8 +147,9 @@ class PanelSpy:
"active_dataptr": active_dataptr,
"active_propname": active_propname,
}
self.spied_lists.append(spied_data)
return TemplateListSpy(spied_data)
template_list = TemplateListSpy(getattr(bpy.types, listtype_name), spied_data)
self.spied_lists.append(template_list)
return template_list
elif self.spied_attr == "context_pointer_set":
return lambda *args, **kwargs: None
elif self.spied_attr == "label":
@@ -157,9 +219,39 @@ class OperatorSpy:
self.spied_data["kwargs"][name] = value
class TemplateListSpy:
def __init__(self, spied_data):
class TemplateListSpy(PanelSpy):
def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict):
self.spied_data = spied_data
self.items = getattr(self.spied_data["dataptr"], self.spied_data["propname"])
self.active_index = getattr(self.spied_data["active_dataptr"], self.spied_data["active_propname"])
try:
self.active_item = self.items[self.active_index]
except:
self.active_item = None
self.panel = template_list
self.rows = []
for item in self.items:
self.rows.append(TemplateListItemSpy(self, item))
class TemplateListItemSpy(PanelSpy):
def __init__(self, parent: TemplateListSpy, item):
self.panel = parent.panel
self.spied_attr: Union[str, None] = None
self.spied_labels: list[str] = []
self.spied_props: list[dict[str, Any]] = []
self.spied_operators: list[dict[str, Any]] = []
parent.panel.draw_item(
self,
bpy.context,
self,
parent.spied_data["dataptr"],
item,
"",
parent.spied_data["active_dataptr"],
parent.spied_data["active_propname"],
)
ui_name_cache = {}
@@ -181,6 +273,8 @@ def create_ui_name_cache():
if panel_type.bl_label == "Add" and bl_idname != "VIEW3D_MT_add":
continue # Non-unique, but "VIEW3D_MT_add" is the one we care about
ui_name_cache[panel_type.bl_label] = bl_idname
elif panel_type.bl_rna.base.name == "UIList":
ui_name_cache[panel_type.bl_rna.name] = bl_idname
except:
pass
@@ -293,6 +387,57 @@ def i_see_text(text):
assert [l for l in panel_spy.spied_labels if text in l], f"Text {text} not found in {panel_spy.spied_labels}"
@given(parsers.parse('I see "{text}" in the "{nth}" list'))
@when(parsers.parse('I see "{text}" in the "{nth}" list'))
@then(parsers.parse('I see "{text}" in the "{nth}" list'))
def i_see_text_in_the_nth_list(text, nth):
assert panel_spy
panel_spy.refresh_spy()
nth = int("".join([c for c in nth if c.isnumeric()]))
if len(panel_spy.spied_lists) < nth:
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
debug = []
for i, template_list in enumerate(panel_spy.spied_lists):
if i + 1 != nth:
continue
for row in template_list.rows:
for l in row.spied_labels:
debug.append(l)
if text in l:
return True
debug = "\n".join(debug)
assert False, f"Could not see '{text}' in any list. We saw:\n{debug}"
@given(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
@when(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
@then(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button, text, nth):
"""
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
nth = int("".join([c for c in nth if c.isnumeric()]))
if len(panel_spy.spied_lists) < nth:
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
debug = []
for i, template_list in enumerate(panel_spy.spied_lists):
if i + 1 != nth:
continue
for row in template_list.rows:
is_row = False
for l in row.spied_labels:
debug.append(l)
if text in l:
is_row = True
if is_row:
i_click_button_on_panel(button, row)
return True
debug = "\n".join(debug)
assert False, f"Could not see '{text}' in any list. We saw:\n{debug}"
@given(parsers.parse('I don\'t see "{text}"'))
@when(parsers.parse('I don\'t see "{text}"'))
@then(parsers.parse('I don\'t see "{text}"'))
@@ -564,15 +709,7 @@ def i_press_operator(operator):
assert False, f"Failed to run operator bpy.ops.{operator} because of {e}"
@given(parsers.parse('I click "{button}"'))
@when(parsers.parse('I click "{button}"'))
@then(parsers.parse('I click "{button}"'))
def i_click_button(button):
"""
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
def i_click_button_on_panel(button, panel_spy):
for spied_operator in panel_spy.spied_operators:
if spied_operator["text"] == button or spied_operator["icon"] == button:
spied_operator["operator"]("INVOKE_DEFAULT", **spied_operator["kwargs"])
@@ -592,6 +729,18 @@ def i_click_button(button):
assert False, f"Could not find {button}:\n{debug}"
@given(parsers.parse('I click "{button}"'))
@when(parsers.parse('I click "{button}"'))
@then(parsers.parse('I click "{button}"'))
def i_click_button(button):
"""
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
i_click_button_on_panel(button, panel_spy)
@given(parsers.parse('I click the "{button}" after the text "{text}"'))
@when(parsers.parse('I click the "{button}" after the text "{text}"'))
@then(parsers.parse('I click the "{button}" after the text "{text}"'))