Skip to content

Commit 98e7c24

Browse files
committed
Fix bugs during GUI testing
* 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
1 parent f5ecf0d commit 98e7c24

34 files changed

+279
-67
lines changed

src/objdictgen/__main__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
311311
print(f"{objdictgen.ODG_PROGRAM}: '{opts.od1}' and '{opts.od2}' are equal")
312312

313313
print_diffs(diffs, show=opts.show)
314-
parser.exit(errcode)
314+
if errcode:
315+
parser.exit(errcode)
315316

316317

317318
# -- EDIT command --

src/objdictgen/eds_utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ def generate_eds_content(node: "Node", filepath: TPath):
655655
if 0x2000 <= entry <= 0x5FFF:
656656
manufacturers.append(entry)
657657
# Second case, entry is required, then it's a mandatory entry
658-
elif entry_infos["need"]:
658+
elif entry_infos.get("need"):
659659
mandatories.append(entry)
660660
# In any other case, it's an optional entry
661661
else:

src/objdictgen/maps.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,33 @@ class ODStructTypes:
6767
#
6868
# Properties of entry structure in the Object Dictionary
6969
#
70-
Subindex = 1 # Entry has at least one subindex
71-
MultipleSubindexes = 2 # Entry has more than one subindex
72-
IdenticalSubindexes = 4 # Subindexes of entry have the same description
73-
IdenticalIndexes = 8 # Entry has the same description on multiple indexes
70+
Subindex = 1
71+
"""Entry has at least one subindex"""
72+
MultipleSubindexes = 2
73+
"""Entry has more than one subindex"""
74+
IdenticalSubindexes = 4
75+
"""Subindexes of entry have the same description"""
76+
IdenticalIndexes = 8
77+
"""Entry has the same description on multiple objects"""
7478

7579
#
7680
# Structures of entry in the Object Dictionary, sum of the properties described above
7781
# for all sorts of entries use in CAN Open specification
7882
#
7983
NOSUB = 0 # Entry without subindex (only for type declaration)
8084
VAR = Subindex # 1
85+
"""Variable object structure"""
8186
RECORD = Subindex | MultipleSubindexes # 3
87+
"""Record object structure, i.e. subindexes with different descriptions"""
8288
ARRAY = Subindex | MultipleSubindexes | IdenticalSubindexes # 7
89+
"""Array object structure, i.e. subindexes with the same type"""
8390
# Entries identical on multiple indexes
8491
NVAR = Subindex | IdenticalIndexes # 9
92+
"""Variable object structure that spans several objects"""
8593
NRECORD = Subindex | MultipleSubindexes | IdenticalIndexes # 11, Example : PDO Parameters
94+
"""Record object structure that spans several objects"""
8695
NARRAY = Subindex | MultipleSubindexes | IdenticalSubindexes | IdenticalIndexes # 15, Example : PDO Mapping
96+
"""Array object structure that spans several objects"""
8797

8898
#
8999
# Mapping against name and structure number
@@ -425,7 +435,7 @@ def FindMandatoryIndexes(self) -> list[int]:
425435
return [
426436
index
427437
for index in self
428-
if index >= 0x1000 and self[index]["need"]
438+
if index >= 0x1000 and self[index].get("need")
429439
]
430440

431441
def FindEntryName(self, index: int, compute=True) -> str:

src/objdictgen/node.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import copy
2222
import logging
23+
from pathlib import Path
2324
from typing import Any, Generator, Iterable, Iterator
2425

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

198-
def DumpFile(self, filepath: TPath, filetype: str = "json", **kwargs):
199+
def DumpFile(self, filepath: TPath, filetype: str|None = "json", **kwargs):
199200
""" Save node into file """
201+
202+
# Attempt to determine the filetype from the filepath
203+
if not filetype:
204+
filetype = Path(filepath).suffix[1:]
205+
if not filetype:
206+
filetype = "json"
207+
200208
if filetype == 'od':
201209
log.debug("Writing XML OD '%s'", filepath)
202210
with open(filepath, "w", encoding="utf-8") as f:
@@ -860,8 +868,11 @@ def RemoveMappingEntry(self, index: int, subindex: int|None = None):
860868
if subindex is None:
861869
self.UserMapping.pop(index)
862870
return
863-
if subindex == len(self.UserMapping[index]["values"]) - 1:
864-
self.UserMapping[index]["values"].pop(subindex)
871+
obj = self.UserMapping[index]
872+
if subindex == len(obj["values"]) - 1:
873+
obj["values"].pop(subindex)
874+
return
875+
if obj['struct'] & OD.IdenticalSubindexes:
865876
return
866877
raise ValueError(f"Invalid subindex {subindex} for index 0x{index:04x}")
867878

src/objdictgen/nodemanager.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,7 @@ def RemoveSubentriesFromCurrent(self, index: int, number: int):
377377
length = node.GetEntry(index, 0)
378378
# FIXME: This code assumes that subindex 0 is the length of the entry
379379
assert isinstance(length, int)
380-
if "nbmin" in infos:
381-
nbmin = infos["nbmin"]
382-
else:
383-
nbmin = 1
380+
nbmin = infos.get("nbmin", 1)
384381
# Entry is an array, or is an array/record of manufacturer specific
385382
# FIXME: What is the intended order of the conditions? or-and on same level
386383
if (infos["struct"] & OD.IdenticalSubindexes or 0x2000 <= index <= 0x5FFF
@@ -574,7 +571,7 @@ def RemoveCurrentVariable(self, index: int, subindex: int|None = None):
574571
node.RemoveMapVariable(index, subindex or 0)
575572
if not found:
576573
infos = node.GetEntryInfos(index)
577-
if not infos["need"]:
574+
if not infos.get("need"):
578575
node.RemoveEntry(index, subindex)
579576
if index in mappings[-1]:
580577
node.RemoveMappingEntry(index, subindex)
@@ -700,8 +697,7 @@ def SetCurrentEntry(self, index: int, subindex: int, value: str, name: str, edit
700697
# Might fail with binascii.Error if hex is malformed
701698
if len(value) % 2 != 0:
702699
value = "0" + value
703-
# FIXME: decode() produce bytes, which is not supported as of now
704-
bvalue = codecs.decode(value, 'hex_codec')
700+
bvalue = codecs.decode(value, 'hex_codec').decode()
705701
node.SetEntry(index, subindex, bvalue)
706702
elif editor == "dcf":
707703
node.SetEntry(index, subindex, value)
@@ -737,12 +733,14 @@ def SetCurrentEntry(self, index: int, subindex: int, value: str, name: str, edit
737733
raise ValueError("Number must be positive")
738734
node.SetParamsEntry(index, subindex, params={"buffer_size": nvalue})
739735
else:
736+
nvalue: str|int = value
740737
if editor == "type":
741738
nvalue = node.GetTypeIndex(value)
739+
# All type object shall have size
742740
size = node.GetEntryInfos(nvalue)["size"]
743741
node.UpdateMapVariable(index, subindex, size)
744742
elif editor in ["access", "raccess"]:
745-
value = {
743+
nvalue = { # type: ignore[assignment]
746744
access: abbrev
747745
for abbrev, access in maps.ACCESS_TYPE.items()
748746
}[value]
@@ -753,7 +751,7 @@ def SetCurrentEntry(self, index: int, subindex: int, value: str, name: str, edit
753751
node.AddMappingEntry(index, entry={"name": entry_infos["name"], "struct": OD.ARRAY})
754752
node.AddMappingSubEntry(index, 0, values=subindex0_infos)
755753
node.AddMappingSubEntry(index, 1, values=subindex1_infos)
756-
node.SetMappingSubEntry(index, subindex, values={name: value}) # type: ignore[misc]
754+
node.SetMappingSubEntry(index, subindex, values={name: nvalue}) # type: ignore[misc]
757755
if not disable_buffer:
758756
self.BufferCurrentNode()
759757

@@ -1049,7 +1047,7 @@ def GetNodeEntryValues(self, node: Node, index: int) -> tuple[list[dict], list[d
10491047
editor["value"] = "dcf"
10501048
else:
10511049
editor["value"] = "domain"
1052-
dic["value"] = codecs.encode(dic["value"].encode(), 'hex_codec')
1050+
dic["value"] = codecs.encode(dic["value"].encode(), 'hex_codec').decode()
10531051
elif dic["type"] == "BOOLEAN":
10541052
editor["value"] = "bool"
10551053
dic["value"] = maps.BOOL_TYPE[dic["value"]]
@@ -1065,7 +1063,7 @@ def GetNodeEntryValues(self, node: Node, index: int) -> tuple[list[dict], list[d
10651063
raise # FIXME: Originial code swallows exception
10661064
try:
10671065
dic["value"] = fmt.format(dic["value"])
1068-
except TypeError as exc:
1066+
except ValueError as exc:
10691067
log.debug("ValueError: '%s': %s", dic["value"], exc)
10701068
# FIXME: dict["value"] can contain $NODEID for PDOs i.e. $NODEID+0x200
10711069
editor["value"] = "string"

src/objdictgen/ui/nodeeditortemplate.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def SetStatusBarText(self, selection, node: Node):
8989
entryinfos = node.GetEntryInfos(index)
9090
name = entryinfos["name"]
9191
category = "Optional"
92-
if entryinfos["need"]:
92+
if entryinfos.get("need"):
9393
category = "Mandatory"
9494
struct = "VAR"
9595
number = ""

src/objdictgen/ui/subindextable.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def _init_ctrls(self, parent):
432432

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

@@ -729,7 +729,10 @@ def ShowDCFEntryDialog(self, row, col):
729729
dialog.SetValues(codecs.decode(self.Table.GetValue(row, col), "hex_codec"))
730730
if dialog.ShowModal() == wx.ID_OK and self.Editable:
731731
value = dialog.GetValues()
732-
self.Manager.SetCurrentEntry(index, row, value, "value", "dcf")
732+
try:
733+
self.Manager.SetCurrentEntry(index, row, value, "value", "dcf")
734+
except Exception as e: # pylint: disable=broad-except
735+
display_error_dialog(self, f"Failed to set value: {e}", "Failed to set value")
733736
self.ParentWindow.RefreshBufferState()
734737
wx.CallAfter(self.RefreshTable)
735738

@@ -741,7 +744,10 @@ def OnSubindexGridCellChange(self, event):
741744
name = self.Table.GetColLabelValue(col, False)
742745
value = self.Table.GetValue(subindex, col, False)
743746
editor = self.Table.GetEditor(subindex, col)
744-
self.Manager.SetCurrentEntry(index, subindex, value, name, editor)
747+
try:
748+
self.Manager.SetCurrentEntry(index, subindex, value, name, editor)
749+
except Exception as e: # pylint: disable=broad-except
750+
display_error_dialog(self, f"Failed to set value: {e}", "Failed to set value")
745751
self.ParentWindow.RefreshBufferState()
746752
wx.CallAfter(self.RefreshTable)
747753
event.Skip()

tests/conftest.py

+24
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,25 @@
3939
ODDIR / "legacy-strings.od", # The legacy tool silently crash on this input
4040
]
4141

42+
# Equivalent files that should compare as equal
43+
COMPARE_EQUIVS = [
44+
('alltypes', 'legacy-alltypes'),
45+
('master', 'legacy-master'),
46+
('slave', 'legacy-slave'),
47+
#( "profile-test", "legacy-profile-test"),
48+
( "profile-ds302", "legacy-profile-ds302"),
49+
( "profile-ds401", "legacy-profile-ds401"),
50+
( "profile-ds302-ds401", "legacy-profile-ds302-ds401"),
51+
#( "profile-ds302-test", "legacy-profile-ds302-test"),
52+
( "slave-ds302", "legacy-slave-ds302"),
53+
( "slave-emcy", "legacy-slave-emcy"),
54+
( "slave-heartbeat", "legacy-slave-heartbeat"),
55+
( "slave-nodeguarding", "legacy-slave-nodeguarding"),
56+
( "slave-sync", "legacy-slave-sync"),
57+
( "strings", "legacy-strings"),
58+
( "domain", "legacy-domain"),
59+
]
60+
4261

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

246+
# Add "equiv_files" fixture
247+
if "equiv_files" in metafunc.fixturenames:
248+
metafunc.parametrize("equiv_files", COMPARE_EQUIVS, ids=(e[0] for e in COMPARE_EQUIVS),
249+
indirect=False, scope="session")
250+
227251

228252
def pytest_collection_modifyitems(items):
229253
"""Modifies test items in place to ensure test modules run in a given order."""

tests/od/domain.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$id": "od data",
3+
"$version": "1",
4+
"$description": "Canfestival object dictionary data",
5+
"$tool": "odg 3.4",
6+
"$date": "2024-04-10T22:48:13.333406",
7+
"name": "domain",
8+
"description": "",
9+
"type": "master",
10+
"id": 0,
11+
"profile": "None",
12+
"dictionary": [
13+
{
14+
"index": "0x2000", // 8192
15+
"name": "Domain",
16+
"struct": "var",
17+
"group": "user",
18+
"mandatory": false,
19+
"sub": [
20+
{
21+
"name": "Domain",
22+
"type": "DOMAIN", // 15
23+
"access": "rw",
24+
"pdo": true,
25+
"value": "\u0002@ABC\u0001"
26+
}
27+
]
28+
}
29+
]
30+
}

tests/od/domain.od

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE PyObject SYSTEM "PyObjects.dtd">
3+
<PyObject module="node" class="Node" id="2217354715472">
4+
<attr name="Profile" type="dict" id="2217369299472">
5+
</attr>
6+
<attr name="Description" type="string" value="" />
7+
<attr name="Dictionary" type="dict" id="2217369505408">
8+
<entry>
9+
<key type="numeric" value="8192" />
10+
<val type="string" value="\x02@ABC\x01" />
11+
</entry>
12+
</attr>
13+
<attr name="SpecificMenu" type="list" id="2217369263296">
14+
</attr>
15+
<attr name="ParamsDictionary" type="dict" id="2217369504960">
16+
</attr>
17+
<attr name="UserMapping" type="dict" id="2217355415632">
18+
<entry>
19+
<key type="numeric" value="8192" />
20+
<val type="dict" id="2217369504832">
21+
<entry>
22+
<key type="string" value="need" />
23+
<val type="False" value="" />
24+
</entry>
25+
<entry>
26+
<key type="string" value="values" />
27+
<val type="list" id="2217369261952">
28+
<item type="dict" id="2217352937216">
29+
<entry>
30+
<key type="string" value="access" />
31+
<val type="string" value="rw" />
32+
</entry>
33+
<entry>
34+
<key type="string" value="pdo" />
35+
<val type="True" value="" />
36+
</entry>
37+
<entry>
38+
<key type="string" value="type" />
39+
<val type="numeric" value="15" />
40+
</entry>
41+
<entry>
42+
<key type="string" value="name" />
43+
<val type="string" value="Domain" />
44+
</entry>
45+
</item>
46+
</val>
47+
</entry>
48+
<entry>
49+
<key type="string" value="name" />
50+
<val type="string" value="Domain" />
51+
</entry>
52+
<entry>
53+
<key type="string" value="struct" />
54+
<val type="numeric" value="1" />
55+
</entry>
56+
</val>
57+
</entry>
58+
</attr>
59+
<attr name="DS302" type="dict" id="2217369297168">
60+
</attr>
61+
<attr name="ProfileName" type="string" value="None" />
62+
<attr name="Type" type="string" value="master" />
63+
<attr name="ID" type="numeric" value="0" />
64+
<attr name="Name" type="string" value="domain" />
65+
</PyObject>

0 commit comments

Comments
 (0)