Skip to content

Commit

Permalink
Fix bugs during GUI testing
Browse files Browse the repository at this point in the history
* Fix failure on reading the "need" field from node
* Increase the viewport size of the index parts in GUI
* Add error message if entering invalid data in GUI table editor
* Fix bug where UI changes in the table didn't update the data
* Added in-line documentation in ODStructTypes
* Fix incorrect ValueError when having arrays
* FIxed Node.DumpFile not guessing the filetype correctly
* Fix handling of DOMAIN, which address #10
Testing
* Add new "equiv_files" fixture to test equivalent files
* Add test for testing `odg compare`
* Added OD-file for DOMAIN and updated OD-files for consistency
  • Loading branch information
sveinse committed Apr 10, 2024
1 parent f5ecf0d commit 98e7c24
Show file tree
Hide file tree
Showing 34 changed files with 279 additions and 67 deletions.
3 changes: 2 additions & 1 deletion src/objdictgen/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
print(f"{objdictgen.ODG_PROGRAM}: '{opts.od1}' and '{opts.od2}' are equal")

print_diffs(diffs, show=opts.show)
parser.exit(errcode)
if errcode:
parser.exit(errcode)

Check warning on line 315 in src/objdictgen/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/__main__.py#L315

Added line #L315 was not covered by tests


# -- EDIT command --
Expand Down
2 changes: 1 addition & 1 deletion src/objdictgen/eds_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ def generate_eds_content(node: "Node", filepath: TPath):
if 0x2000 <= entry <= 0x5FFF:
manufacturers.append(entry)
# Second case, entry is required, then it's a mandatory entry
elif entry_infos["need"]:
elif entry_infos.get("need"):
mandatories.append(entry)
# In any other case, it's an optional entry
else:
Expand Down
20 changes: 15 additions & 5 deletions src/objdictgen/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,33 @@ class ODStructTypes:
#
# Properties of entry structure in the Object Dictionary
#
Subindex = 1 # Entry has at least one subindex
MultipleSubindexes = 2 # Entry has more than one subindex
IdenticalSubindexes = 4 # Subindexes of entry have the same description
IdenticalIndexes = 8 # Entry has the same description on multiple indexes
Subindex = 1
"""Entry has at least one subindex"""
MultipleSubindexes = 2
"""Entry has more than one subindex"""
IdenticalSubindexes = 4
"""Subindexes of entry have the same description"""
IdenticalIndexes = 8
"""Entry has the same description on multiple objects"""

#
# Structures of entry in the Object Dictionary, sum of the properties described above
# for all sorts of entries use in CAN Open specification
#
NOSUB = 0 # Entry without subindex (only for type declaration)
VAR = Subindex # 1
"""Variable object structure"""
RECORD = Subindex | MultipleSubindexes # 3
"""Record object structure, i.e. subindexes with different descriptions"""
ARRAY = Subindex | MultipleSubindexes | IdenticalSubindexes # 7
"""Array object structure, i.e. subindexes with the same type"""
# Entries identical on multiple indexes
NVAR = Subindex | IdenticalIndexes # 9
"""Variable object structure that spans several objects"""
NRECORD = Subindex | MultipleSubindexes | IdenticalIndexes # 11, Example : PDO Parameters
"""Record object structure that spans several objects"""
NARRAY = Subindex | MultipleSubindexes | IdenticalSubindexes | IdenticalIndexes # 15, Example : PDO Mapping
"""Array object structure that spans several objects"""

#
# Mapping against name and structure number
Expand Down Expand Up @@ -425,7 +435,7 @@ def FindMandatoryIndexes(self) -> list[int]:
return [
index
for index in self
if index >= 0x1000 and self[index]["need"]
if index >= 0x1000 and self[index].get("need")
]

def FindEntryName(self, index: int, compute=True) -> str:
Expand Down
17 changes: 14 additions & 3 deletions src/objdictgen/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import copy
import logging
from pathlib import Path
from typing import Any, Generator, Iterable, Iterator

import colorama
Expand Down Expand Up @@ -195,8 +196,15 @@ def LoadJson(contents: str) -> "Node":
""" Import a new Node from a JSON string """
return jsonod.generate_node(contents)

def DumpFile(self, filepath: TPath, filetype: str = "json", **kwargs):
def DumpFile(self, filepath: TPath, filetype: str|None = "json", **kwargs):
""" Save node into file """

# Attempt to determine the filetype from the filepath
if not filetype:
filetype = Path(filepath).suffix[1:]

Check warning on line 204 in src/objdictgen/node.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/node.py#L204

Added line #L204 was not covered by tests
if not filetype:
filetype = "json"

Check warning on line 206 in src/objdictgen/node.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/node.py#L206

Added line #L206 was not covered by tests

if filetype == 'od':
log.debug("Writing XML OD '%s'", filepath)
with open(filepath, "w", encoding="utf-8") as f:
Expand Down Expand Up @@ -860,8 +868,11 @@ def RemoveMappingEntry(self, index: int, subindex: int|None = None):
if subindex is None:
self.UserMapping.pop(index)
return
if subindex == len(self.UserMapping[index]["values"]) - 1:
self.UserMapping[index]["values"].pop(subindex)
obj = self.UserMapping[index]
if subindex == len(obj["values"]) - 1:
obj["values"].pop(subindex)
return
if obj['struct'] & OD.IdenticalSubindexes:
return
raise ValueError(f"Invalid subindex {subindex} for index 0x{index:04x}")

Check warning on line 877 in src/objdictgen/node.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/node.py#L866-L877

Added lines #L866 - L877 were not covered by tests

Expand Down
20 changes: 9 additions & 11 deletions src/objdictgen/nodemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,7 @@ def RemoveSubentriesFromCurrent(self, index: int, number: int):
length = node.GetEntry(index, 0)

Check warning on line 377 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L375-L377

Added lines #L375 - L377 were not covered by tests
# FIXME: This code assumes that subindex 0 is the length of the entry
assert isinstance(length, int)
if "nbmin" in infos:
nbmin = infos["nbmin"]
else:
nbmin = 1
nbmin = infos.get("nbmin", 1)

Check warning on line 380 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L379-L380

Added lines #L379 - L380 were not covered by tests
# Entry is an array, or is an array/record of manufacturer specific
# FIXME: What is the intended order of the conditions? or-and on same level
if (infos["struct"] & OD.IdenticalSubindexes or 0x2000 <= index <= 0x5FFF

Check warning on line 383 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L383

Added line #L383 was not covered by tests
Expand Down Expand Up @@ -574,7 +571,7 @@ def RemoveCurrentVariable(self, index: int, subindex: int|None = None):
node.RemoveMapVariable(index, subindex or 0)

Check warning on line 571 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L571

Added line #L571 was not covered by tests
if not found:
infos = node.GetEntryInfos(index)
if not infos["need"]:
if not infos.get("need"):
node.RemoveEntry(index, subindex)

Check warning on line 575 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L573-L575

Added lines #L573 - L575 were not covered by tests
if index in mappings[-1]:
node.RemoveMappingEntry(index, subindex)

Check warning on line 577 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L577

Added line #L577 was not covered by tests
Expand Down Expand Up @@ -700,8 +697,7 @@ def SetCurrentEntry(self, index: int, subindex: int, value: str, name: str, edit
# Might fail with binascii.Error if hex is malformed
if len(value) % 2 != 0:
value = "0" + value
# FIXME: decode() produce bytes, which is not supported as of now
bvalue = codecs.decode(value, 'hex_codec')
bvalue = codecs.decode(value, 'hex_codec').decode()
node.SetEntry(index, subindex, bvalue)

Check warning on line 701 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L700-L701

Added lines #L700 - L701 were not covered by tests
elif editor == "dcf":
node.SetEntry(index, subindex, value)
Expand Down Expand Up @@ -737,12 +733,14 @@ def SetCurrentEntry(self, index: int, subindex: int, value: str, name: str, edit
raise ValueError("Number must be positive")
node.SetParamsEntry(index, subindex, params={"buffer_size": nvalue})

Check warning on line 734 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L734

Added line #L734 was not covered by tests
else:
nvalue: str|int = value

Check warning on line 736 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L736

Added line #L736 was not covered by tests
if editor == "type":
nvalue = node.GetTypeIndex(value)

Check warning on line 738 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L738

Added line #L738 was not covered by tests
# All type object shall have size
size = node.GetEntryInfos(nvalue)["size"]

Check warning on line 740 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L740

Added line #L740 was not covered by tests
node.UpdateMapVariable(index, subindex, size)
elif editor in ["access", "raccess"]:
value = {
nvalue = { # type: ignore[assignment]

Check warning on line 743 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L743

Added line #L743 was not covered by tests
access: abbrev
for abbrev, access in maps.ACCESS_TYPE.items()
}[value]
Expand All @@ -753,7 +751,7 @@ def SetCurrentEntry(self, index: int, subindex: int, value: str, name: str, edit
node.AddMappingEntry(index, entry={"name": entry_infos["name"], "struct": OD.ARRAY})
node.AddMappingSubEntry(index, 0, values=subindex0_infos)
node.AddMappingSubEntry(index, 1, values=subindex1_infos)
node.SetMappingSubEntry(index, subindex, values={name: value}) # type: ignore[misc]
node.SetMappingSubEntry(index, subindex, values={name: nvalue}) # type: ignore[misc]

Check warning on line 754 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L748-L754

Added lines #L748 - L754 were not covered by tests
if not disable_buffer:
self.BufferCurrentNode()

Expand Down Expand Up @@ -1049,7 +1047,7 @@ def GetNodeEntryValues(self, node: Node, index: int) -> tuple[list[dict], list[d
editor["value"] = "dcf"
else:
editor["value"] = "domain"
dic["value"] = codecs.encode(dic["value"].encode(), 'hex_codec')
dic["value"] = codecs.encode(dic["value"].encode(), 'hex_codec').decode()

Check warning on line 1050 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L1050

Added line #L1050 was not covered by tests
elif dic["type"] == "BOOLEAN":
editor["value"] = "bool"
dic["value"] = maps.BOOL_TYPE[dic["value"]]
Expand All @@ -1065,7 +1063,7 @@ def GetNodeEntryValues(self, node: Node, index: int) -> tuple[list[dict], list[d
raise # FIXME: Originial code swallows exception
try:
dic["value"] = fmt.format(dic["value"])
except TypeError as exc:
except ValueError as exc:
log.debug("ValueError: '%s': %s", dic["value"], exc)

Check warning on line 1067 in src/objdictgen/nodemanager.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/nodemanager.py#L1065-L1067

Added lines #L1065 - L1067 were not covered by tests
# FIXME: dict["value"] can contain $NODEID for PDOs i.e. $NODEID+0x200
editor["value"] = "string"
Expand Down
2 changes: 1 addition & 1 deletion src/objdictgen/ui/nodeeditortemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def SetStatusBarText(self, selection, node: Node):
entryinfos = node.GetEntryInfos(index)

Check warning on line 89 in src/objdictgen/ui/nodeeditortemplate.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/ui/nodeeditortemplate.py#L86-L89

Added lines #L86 - L89 were not covered by tests
name = entryinfos["name"]
category = "Optional"
if entryinfos["need"]:
if entryinfos.get("need"):

Check warning on line 92 in src/objdictgen/ui/nodeeditortemplate.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/ui/nodeeditortemplate.py#L92

Added line #L92 was not covered by tests
category = "Mandatory"
struct = "VAR"
number = ""
Expand Down
12 changes: 9 additions & 3 deletions src/objdictgen/ui/subindextable.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def _init_ctrls(self, parent):

self.PartList = wx.ListBox(choices=[], id=ID_EDITINGPANELPARTLIST,
name='PartList', parent=self, pos=wx.Point(0, 0),
size=wx.Size(-1, -1), style=0)
size=wx.Size(-1, 180), style=0)
self.PartList.Bind(wx.EVT_LISTBOX, self.OnPartListBoxClick,
id=ID_EDITINGPANELPARTLIST)

Expand Down Expand Up @@ -729,7 +729,10 @@ def ShowDCFEntryDialog(self, row, col):
dialog.SetValues(codecs.decode(self.Table.GetValue(row, col), "hex_codec"))
if dialog.ShowModal() == wx.ID_OK and self.Editable:
value = dialog.GetValues()
self.Manager.SetCurrentEntry(index, row, value, "value", "dcf")
try:
self.Manager.SetCurrentEntry(index, row, value, "value", "dcf")
except Exception as e: # pylint: disable=broad-except
display_error_dialog(self, f"Failed to set value: {e}", "Failed to set value")

Check warning on line 735 in src/objdictgen/ui/subindextable.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/ui/subindextable.py#L732-L735

Added lines #L732 - L735 were not covered by tests
self.ParentWindow.RefreshBufferState()
wx.CallAfter(self.RefreshTable)

Expand All @@ -741,7 +744,10 @@ def OnSubindexGridCellChange(self, event):
name = self.Table.GetColLabelValue(col, False)
value = self.Table.GetValue(subindex, col, False)
editor = self.Table.GetEditor(subindex, col)
self.Manager.SetCurrentEntry(index, subindex, value, name, editor)
try:
self.Manager.SetCurrentEntry(index, subindex, value, name, editor)
except Exception as e: # pylint: disable=broad-except
display_error_dialog(self, f"Failed to set value: {e}", "Failed to set value")

Check warning on line 750 in src/objdictgen/ui/subindextable.py

View check run for this annotation

Codecov / codecov/patch

src/objdictgen/ui/subindextable.py#L747-L750

Added lines #L747 - L750 were not covered by tests
self.ParentWindow.RefreshBufferState()
wx.CallAfter(self.RefreshTable)
event.Skip()
Expand Down
24 changes: 24 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@
ODDIR / "legacy-strings.od", # The legacy tool silently crash on this input
]

# Equivalent files that should compare as equal
COMPARE_EQUIVS = [
('alltypes', 'legacy-alltypes'),
('master', 'legacy-master'),
('slave', 'legacy-slave'),
#( "profile-test", "legacy-profile-test"),
( "profile-ds302", "legacy-profile-ds302"),
( "profile-ds401", "legacy-profile-ds401"),
( "profile-ds302-ds401", "legacy-profile-ds302-ds401"),
#( "profile-ds302-test", "legacy-profile-ds302-test"),
( "slave-ds302", "legacy-slave-ds302"),
( "slave-emcy", "legacy-slave-emcy"),
( "slave-heartbeat", "legacy-slave-heartbeat"),
( "slave-nodeguarding", "legacy-slave-nodeguarding"),
( "slave-sync", "legacy-slave-sync"),
( "strings", "legacy-strings"),
( "domain", "legacy-domain"),
]


class ODPath(type(Path())):
""" Overload on Path to add OD specific methods """
Expand Down Expand Up @@ -224,6 +243,11 @@ def odids(odlist):
metafunc.parametrize("py2", [Py2(py2_path, objdictgen_dir)],
indirect=False, scope="session")

# Add "equiv_files" fixture
if "equiv_files" in metafunc.fixturenames:
metafunc.parametrize("equiv_files", COMPARE_EQUIVS, ids=(e[0] for e in COMPARE_EQUIVS),
indirect=False, scope="session")


def pytest_collection_modifyitems(items):
"""Modifies test items in place to ensure test modules run in a given order."""
Expand Down
30 changes: 30 additions & 0 deletions tests/od/domain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$id": "od data",
"$version": "1",
"$description": "Canfestival object dictionary data",
"$tool": "odg 3.4",
"$date": "2024-04-10T22:48:13.333406",
"name": "domain",
"description": "",
"type": "master",
"id": 0,
"profile": "None",
"dictionary": [
{
"index": "0x2000", // 8192
"name": "Domain",
"struct": "var",
"group": "user",
"mandatory": false,
"sub": [
{
"name": "Domain",
"type": "DOMAIN", // 15
"access": "rw",
"pdo": true,
"value": "\u0002@ABC\u0001"
}
]
}
]
}
65 changes: 65 additions & 0 deletions tests/od/domain.od
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0"?>
<!DOCTYPE PyObject SYSTEM "PyObjects.dtd">
<PyObject module="node" class="Node" id="2217354715472">
<attr name="Profile" type="dict" id="2217369299472">
</attr>
<attr name="Description" type="string" value="" />
<attr name="Dictionary" type="dict" id="2217369505408">
<entry>
<key type="numeric" value="8192" />
<val type="string" value="\x02@ABC\x01" />
</entry>
</attr>
<attr name="SpecificMenu" type="list" id="2217369263296">
</attr>
<attr name="ParamsDictionary" type="dict" id="2217369504960">
</attr>
<attr name="UserMapping" type="dict" id="2217355415632">
<entry>
<key type="numeric" value="8192" />
<val type="dict" id="2217369504832">
<entry>
<key type="string" value="need" />
<val type="False" value="" />
</entry>
<entry>
<key type="string" value="values" />
<val type="list" id="2217369261952">
<item type="dict" id="2217352937216">
<entry>
<key type="string" value="access" />
<val type="string" value="rw" />
</entry>
<entry>
<key type="string" value="pdo" />
<val type="True" value="" />
</entry>
<entry>
<key type="string" value="type" />
<val type="numeric" value="15" />
</entry>
<entry>
<key type="string" value="name" />
<val type="string" value="Domain" />
</entry>
</item>
</val>
</entry>
<entry>
<key type="string" value="name" />
<val type="string" value="Domain" />
</entry>
<entry>
<key type="string" value="struct" />
<val type="numeric" value="1" />
</entry>
</val>
</entry>
</attr>
<attr name="DS302" type="dict" id="2217369297168">
</attr>
<attr name="ProfileName" type="string" value="None" />
<attr name="Type" type="string" value="master" />
<attr name="ID" type="numeric" value="0" />
<attr name="Name" type="string" value="domain" />
</PyObject>
Loading

0 comments on commit 98e7c24

Please sign in to comment.