odxtools 6.4.2__py3-none-any.whl → 6.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odxtools/basicstructure.py +7 -3
- odxtools/cli/_print_utils.py +210 -25
- odxtools/cli/compare.py +730 -0
- odxtools/cli/decode.py +1 -1
- odxtools/cli/find.py +13 -4
- odxtools/cli/list.py +74 -40
- odxtools/cli/main.py +1 -1
- odxtools/database.py +1 -0
- odxtools/dataobjectproperty.py +15 -2
- odxtools/diagcodedtype.py +85 -67
- odxtools/internalconstr.py +34 -0
- odxtools/leadinglengthinfotype.py +5 -5
- odxtools/minmaxlengthtype.py +3 -3
- odxtools/paramlengthinfotype.py +2 -2
- odxtools/scaleconstr.py +50 -0
- odxtools/standardlengthtype.py +2 -2
- odxtools/templates/macros/printDOP.xml.jinja2 +52 -5
- odxtools/version.py +2 -2
- {odxtools-6.4.2.dist-info → odxtools-6.5.0.dist-info}/METADATA +388 -47
- {odxtools-6.4.2.dist-info → odxtools-6.5.0.dist-info}/RECORD +24 -21
- {odxtools-6.4.2.dist-info → odxtools-6.5.0.dist-info}/LICENSE +0 -0
- {odxtools-6.4.2.dist-info → odxtools-6.5.0.dist-info}/WHEEL +0 -0
- {odxtools-6.4.2.dist-info → odxtools-6.5.0.dist-info}/entry_points.txt +0 -0
- {odxtools-6.4.2.dist-info → odxtools-6.5.0.dist-info}/top_level.txt +0 -0
odxtools/cli/compare.py
ADDED
@@ -0,0 +1,730 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
|
4
|
+
import argparse
|
5
|
+
import os
|
6
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
7
|
+
|
8
|
+
from rich import print
|
9
|
+
from tabulate import tabulate # TODO: switch to rich tables
|
10
|
+
|
11
|
+
from ..database import Database
|
12
|
+
from ..diaglayer import DiagLayer
|
13
|
+
from ..diagservice import DiagService
|
14
|
+
from ..load_file import load_file
|
15
|
+
from ..odxtypes import AtomicOdxType
|
16
|
+
from ..parameters.codedconstparameter import CodedConstParameter
|
17
|
+
from ..parameters.nrcconstparameter import NrcConstParameter
|
18
|
+
from ..parameters.parameter import Parameter
|
19
|
+
from ..parameters.physicalconstantparameter import PhysicalConstantParameter
|
20
|
+
from ..parameters.valueparameter import ValueParameter
|
21
|
+
from . import _parser_utils
|
22
|
+
from ._print_utils import (extract_service_tabulation_data, print_dl_metrics,
|
23
|
+
print_service_parameters)
|
24
|
+
|
25
|
+
# name of the tool
|
26
|
+
_odxtools_tool_name_ = "compare"
|
27
|
+
|
28
|
+
VariantName = str
|
29
|
+
VariantType = str
|
30
|
+
NewServices = List[DiagService]
|
31
|
+
DeletedServices = List[DiagService]
|
32
|
+
RenamedServices = List[List[Union[str, DiagService]]]
|
33
|
+
ServicesWithParamChanges = List[List[Union[str, DiagService]]]
|
34
|
+
|
35
|
+
SpecsServiceDict = Dict[str, Union[VariantName, VariantType, NewServices, DeletedServices,
|
36
|
+
RenamedServices, ServicesWithParamChanges]]
|
37
|
+
|
38
|
+
NewVariants = List[DiagLayer]
|
39
|
+
DeletedVariants = List[DiagLayer]
|
40
|
+
|
41
|
+
SpecsChangesVariants = Dict[str, Union[NewVariants, DeletedVariants, SpecsServiceDict]]
|
42
|
+
|
43
|
+
|
44
|
+
class Display:
|
45
|
+
# class with variables and functions to display the result of the comparison
|
46
|
+
|
47
|
+
# TODO
|
48
|
+
# Idea: results as json export
|
49
|
+
# - write results of comparison in json structure
|
50
|
+
# - use odxlinks to refer to dignostic services / objects if changes have already been detected (e.g. in another ecu variant / diagnostic layer)
|
51
|
+
|
52
|
+
# print all information about parameter properties (request, pos. response & neg. response parameters) for changed diagnostic services
|
53
|
+
param_detailed: bool
|
54
|
+
obj_detailed: bool
|
55
|
+
|
56
|
+
def __init__(self) -> None:
|
57
|
+
pass
|
58
|
+
|
59
|
+
def print_dl_changes(self, service_dict: SpecsServiceDict) -> None:
|
60
|
+
|
61
|
+
if service_dict["new_services"] or service_dict["deleted_services"] or service_dict[
|
62
|
+
"changed_name_of_service"][0] or service_dict["changed_parameters_of_service"][0]:
|
63
|
+
assert isinstance(service_dict["diag_layer"], str)
|
64
|
+
print(
|
65
|
+
f"\nChanged diagnostic services for diagnostic layer: [u magenta]{service_dict['diag_layer']}[/u magenta] ({service_dict['diag_layer_type']})"
|
66
|
+
)
|
67
|
+
if service_dict["new_services"]:
|
68
|
+
assert isinstance(service_dict["new_services"], List)
|
69
|
+
print("\n [blue]New services[/blue]")
|
70
|
+
table = extract_service_tabulation_data(
|
71
|
+
service_dict["new_services"]) # type: ignore[arg-type]
|
72
|
+
print(tabulate(table, headers='keys', tablefmt='presto'))
|
73
|
+
if service_dict["deleted_services"]:
|
74
|
+
assert isinstance(service_dict["deleted_services"], List)
|
75
|
+
print("\n [blue]Deleted services[/blue]")
|
76
|
+
table = extract_service_tabulation_data(
|
77
|
+
service_dict["deleted_services"]) # type: ignore[arg-type]
|
78
|
+
print(tabulate(table, headers='keys', tablefmt='presto'))
|
79
|
+
if service_dict["changed_name_of_service"][0]:
|
80
|
+
print("\n [blue]Renamed services[/blue]")
|
81
|
+
table = extract_service_tabulation_data(
|
82
|
+
service_dict["changed_name_of_service"][0]) # type: ignore[arg-type]
|
83
|
+
table['Old service name'] = service_dict["changed_name_of_service"][1]
|
84
|
+
print(tabulate(table, headers='keys', tablefmt='presto'))
|
85
|
+
if service_dict["changed_parameters_of_service"][0]:
|
86
|
+
print("\n [blue]Services with parameter changes[/blue]")
|
87
|
+
# create table with information about services with parameter changes
|
88
|
+
table = extract_service_tabulation_data(
|
89
|
+
service_dict["changed_parameters_of_service"][0]) # type: ignore[arg-type]
|
90
|
+
# add information about changed parameters
|
91
|
+
table['Changed parameters'] = service_dict["changed_parameters_of_service"][1]
|
92
|
+
print(tabulate(table, headers='keys', tablefmt='presto'))
|
93
|
+
|
94
|
+
for service_idx, service in enumerate(
|
95
|
+
service_dict["changed_parameters_of_service"][0]): # type: ignore[arg-type]
|
96
|
+
assert isinstance(service, DiagService)
|
97
|
+
print(
|
98
|
+
f"\n Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]"
|
99
|
+
)
|
100
|
+
# detailed_info in [infotext1, dict1, infotext2, dict2, ...]
|
101
|
+
for detailed_info in service_dict["changed_parameters_of_service"][2][
|
102
|
+
service_idx]: # type: ignore[arg-type, index, union-attr]
|
103
|
+
if isinstance(detailed_info, str):
|
104
|
+
print("\n" + detailed_info)
|
105
|
+
elif isinstance(detailed_info, dict):
|
106
|
+
print(tabulate(detailed_info, headers='keys', tablefmt='presto'))
|
107
|
+
if self.param_detailed:
|
108
|
+
# print all parameter details of diagnostic service
|
109
|
+
print_service_parameters(
|
110
|
+
service, allow_unknown_bit_lengths=True, plumbing_output=self.obj_detailed)
|
111
|
+
|
112
|
+
def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None:
|
113
|
+
# prints result of database comparison (input variable: dictionary: changes_variants)
|
114
|
+
|
115
|
+
# diagnostic layers
|
116
|
+
if changes_variants["new_diagnostic_layers"] or changes_variants[
|
117
|
+
"deleted_diagnostic_layers"]:
|
118
|
+
print("\n[bright_blue]Changed diagnostic layers[/bright_blue]: ")
|
119
|
+
print(" New diagnostic layers: ")
|
120
|
+
for variant in changes_variants["new_diagnostic_layers"]:
|
121
|
+
assert isinstance(variant, DiagLayer)
|
122
|
+
print(f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})")
|
123
|
+
print(" Deleted diagnostic layers: ")
|
124
|
+
for variant in changes_variants["deleted_diagnostic_layers"]:
|
125
|
+
assert isinstance(variant, DiagLayer)
|
126
|
+
print(f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})")
|
127
|
+
|
128
|
+
# diagnostic services
|
129
|
+
for _, value in changes_variants.items():
|
130
|
+
if isinstance(value, dict):
|
131
|
+
self.print_dl_changes(value)
|
132
|
+
|
133
|
+
|
134
|
+
class Comparison(Display):
|
135
|
+
databases: List[Database] = [] # storage of database objects
|
136
|
+
diagnostic_layers: List[DiagLayer] = [] # storage of DiagLayer objects
|
137
|
+
diagnostic_layer_names: Set[str] = set() # storage of diagnostic layer names
|
138
|
+
|
139
|
+
db_indicator_1: int
|
140
|
+
db_indicator_2: int
|
141
|
+
|
142
|
+
def __init__(self) -> None:
|
143
|
+
pass
|
144
|
+
|
145
|
+
def compare_parameters(self, param1: Parameter, param2: Parameter) -> Dict[str, Any]:
|
146
|
+
# checks whether properties of param1 and param2 differ
|
147
|
+
# checked properties: Name, Byte Position, Bit Length, Semantic, Parameter Type, Value (Coded, Constant, Default etc.), Data Type, Data Object Property (Name, Physical Data Type, Unit)
|
148
|
+
|
149
|
+
property = []
|
150
|
+
old = []
|
151
|
+
new = []
|
152
|
+
|
153
|
+
def append_list(property_name: str, new_property_value: Optional[AtomicOdxType],
|
154
|
+
old_property_value: Optional[AtomicOdxType]) -> None:
|
155
|
+
property.append(property_name)
|
156
|
+
old.append(old_property_value)
|
157
|
+
new.append(new_property_value)
|
158
|
+
|
159
|
+
if param1.short_name != param2.short_name:
|
160
|
+
append_list('Parameter name', param1.short_name, param2.short_name)
|
161
|
+
if param1.byte_position != param2.byte_position:
|
162
|
+
append_list('Byte position', param1.byte_position, param2.byte_position)
|
163
|
+
if param1.get_static_bit_length() != param2.get_static_bit_length():
|
164
|
+
append_list('Bit Length', param1.get_static_bit_length(),
|
165
|
+
param2.get_static_bit_length())
|
166
|
+
if param1.semantic != param2.semantic:
|
167
|
+
append_list('Semantic', param1.semantic, param2.semantic)
|
168
|
+
if param1.parameter_type != param2.parameter_type:
|
169
|
+
append_list('Parameter type', param1.parameter_type, param2.parameter_type)
|
170
|
+
|
171
|
+
if isinstance(param1, CodedConstParameter) and isinstance(param2, CodedConstParameter):
|
172
|
+
if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type:
|
173
|
+
append_list('Data type', param1.diag_coded_type.base_data_type.name,
|
174
|
+
param2.diag_coded_type.base_data_type.name)
|
175
|
+
if param1.coded_value != param2.coded_value:
|
176
|
+
if isinstance(param1.coded_value, int) and isinstance(param2.coded_value, int):
|
177
|
+
append_list(
|
178
|
+
'Value', "0x" +
|
179
|
+
f"{param1.coded_value:0{(param1.get_static_bit_length() or 0) // 4}X}",
|
180
|
+
"0x" +
|
181
|
+
f"{param2.coded_value:0{(param2.get_static_bit_length() or 0) // 4}X}")
|
182
|
+
else:
|
183
|
+
append_list('Value', f"{param1.coded_value!r}", f"{param2.coded_value!r}")
|
184
|
+
|
185
|
+
elif isinstance(param1, NrcConstParameter) and isinstance(param2, NrcConstParameter):
|
186
|
+
if param1.diag_coded_type.base_data_type != param2.diag_coded_type.base_data_type:
|
187
|
+
append_list('Data type', param1.diag_coded_type.base_data_type.name,
|
188
|
+
param2.diag_coded_type.base_data_type.name)
|
189
|
+
if param1.coded_values != param2.coded_values:
|
190
|
+
append_list('Values', str(param1.coded_values), str(param2.coded_values))
|
191
|
+
|
192
|
+
elif (dop_1 := getattr(param1, "dop", None)) is not None and (dop_2 := getattr(
|
193
|
+
param2, "dop", None)) is not None:
|
194
|
+
|
195
|
+
if dop_1 != dop_2:
|
196
|
+
# TODO: compare INTERNAL-CONSTR, COMPU-INTERNAL-TO-PHYS of DOP
|
197
|
+
append_list('Linked DOP object', "", "")
|
198
|
+
|
199
|
+
# DOP Name
|
200
|
+
if dop_1.short_name != dop_2.short_name:
|
201
|
+
append_list(' DOP name', dop_1.short_name, dop_2.short_name)
|
202
|
+
|
203
|
+
# DOP Unit
|
204
|
+
if getattr(dop_1, "unit", None) and getattr(dop_2, "unit", None):
|
205
|
+
# (properties of unit object: short_name, long_name, description, odx_id, display_name, oid, factor_si_to_unit, offset_si_to_unit, physical_dimension_ref)
|
206
|
+
if dop_1.unit != dop_2.unit and dop_1.unit.short_name != dop_2.unit.short_name:
|
207
|
+
append_list(' DOP unit name', dop_1.unit.short_name, dop_2.unit.short_name)
|
208
|
+
elif dop_1.unit != dop_2.unit and dop_1.unit.display_name != dop_2.unit.display_name:
|
209
|
+
append_list(' DOP unit display name', dop_1.unit.display_name,
|
210
|
+
dop_2.unit.display_name)
|
211
|
+
elif dop_1.unit != dop_2.unit:
|
212
|
+
append_list(' DOP unit object', "", "")
|
213
|
+
|
214
|
+
if hasattr(dop_1, "physical_type") and hasattr(dop_2, "physical_type"):
|
215
|
+
if (dop_1.physical_type and dop_2.physical_type and
|
216
|
+
dop_1.physical_type.base_data_type
|
217
|
+
!= dop_2.physical_type.base_data_type):
|
218
|
+
append_list(' DOP physical data type',
|
219
|
+
dop_1.physical_type.base_data_type.name,
|
220
|
+
dop_2.physical_type.base_data_type.name)
|
221
|
+
|
222
|
+
if (isinstance(param1, PhysicalConstantParameter) and
|
223
|
+
isinstance(param2, PhysicalConstantParameter) and
|
224
|
+
param1.physical_constant_value != param2.physical_constant_value):
|
225
|
+
if isinstance(param1.physical_constant_value, int) and isinstance(
|
226
|
+
param2.physical_constant_value, int):
|
227
|
+
append_list(
|
228
|
+
'Constant value', "0x" +
|
229
|
+
f"{param1.physical_constant_value:0{(param1.get_static_bit_length() or 0) // 4}X}",
|
230
|
+
"0x" +
|
231
|
+
f"{param2.physical_constant_value:0{(param2.get_static_bit_length() or 0) // 4}X}"
|
232
|
+
)
|
233
|
+
else:
|
234
|
+
append_list('Constant value', f"{param1.physical_constant_value!r}",
|
235
|
+
f"{param2.physical_constant_value!r}")
|
236
|
+
|
237
|
+
elif (isinstance(param1, ValueParameter) and isinstance(param2, ValueParameter) and
|
238
|
+
param1.physical_default_value is not None and
|
239
|
+
param2.physical_default_value is not None and
|
240
|
+
param1.physical_default_value != param2.physical_default_value):
|
241
|
+
if isinstance(param1.physical_default_value, int) and isinstance(
|
242
|
+
param2.physical_default_value, int):
|
243
|
+
append_list(
|
244
|
+
'Default value', "0x" +
|
245
|
+
f"{param1.physical_default_value:0{(param1.get_static_bit_length() or 0) // 4}X}",
|
246
|
+
"0x" +
|
247
|
+
f"{param2.physical_default_value:0{(param2.get_static_bit_length() or 0) // 4}X}"
|
248
|
+
)
|
249
|
+
else:
|
250
|
+
append_list('Default value', f"{param1.physical_default_value!r}",
|
251
|
+
f"{param2.physical_default_value!r}")
|
252
|
+
|
253
|
+
return {'Property': property, 'Old Value': old, 'New Value': new}
|
254
|
+
|
255
|
+
def compare_services(self, service1: DiagService, service2: DiagService) -> list:
|
256
|
+
# compares request, positive response and negative response parameters of two diagnostic services
|
257
|
+
|
258
|
+
information: List[Union[str, Dict[str, Any]]] = [
|
259
|
+
] # information = [infotext1, table1, infotext2, table2, ...]
|
260
|
+
changed_params = ''
|
261
|
+
|
262
|
+
# Request
|
263
|
+
if service1.request is not None and service2.request is not None and len(
|
264
|
+
service1.request.parameters) == len(service2.request.parameters):
|
265
|
+
for res1_idx, param1 in enumerate(service1.request.parameters):
|
266
|
+
for res2_idx, param2 in enumerate(service2.request.parameters):
|
267
|
+
if res1_idx == res2_idx:
|
268
|
+
# find changed request parameter properties
|
269
|
+
table = self.compare_parameters(param1, param2)
|
270
|
+
infotext = (' Properties of ' + str(res1_idx + 1) +
|
271
|
+
'. request parameter [b]' + param2.short_name +
|
272
|
+
' have changed\n')
|
273
|
+
# array index starts with 0 -> param[0] is 1. service parameter
|
274
|
+
|
275
|
+
if table['Property']:
|
276
|
+
information.append(infotext)
|
277
|
+
information.append(table)
|
278
|
+
changed_params += 'request parameter \'' + param2.short_name + '\',\n'
|
279
|
+
else:
|
280
|
+
changed_params += 'request parameter list, '
|
281
|
+
# infotext
|
282
|
+
information.append('List of request parameters for service ' + service2.short_name +
|
283
|
+
' is not identical.\n')
|
284
|
+
|
285
|
+
# table
|
286
|
+
|
287
|
+
param_list1 = [] if service1.request is None else service1.request.parameters
|
288
|
+
param_list2 = [] if service2.request is None else service2.request.parameters
|
289
|
+
|
290
|
+
information.append({
|
291
|
+
'List': ['Old list', 'New list'],
|
292
|
+
'Values': [f"\\{param_list1}", f"\\{param_list2}"]
|
293
|
+
})
|
294
|
+
|
295
|
+
# Positive Responses
|
296
|
+
if len(service1.positive_responses) == len(service2.positive_responses):
|
297
|
+
for res1_idx, response1 in enumerate(service1.positive_responses):
|
298
|
+
for res2_idx, response2 in enumerate(service2.positive_responses):
|
299
|
+
if res1_idx == res2_idx:
|
300
|
+
if len(response1.parameters) == len(response2.parameters):
|
301
|
+
for param1_idx, param1 in enumerate(response1.parameters):
|
302
|
+
for param2_idx, param2 in enumerate(response2.parameters):
|
303
|
+
if param1_idx == param2_idx:
|
304
|
+
# find changed positive response parameter properties
|
305
|
+
table = self.compare_parameters(param1, param2)
|
306
|
+
infotext = (' Properties of ' + str(param1_idx + 1) +
|
307
|
+
'. positive response parameter [b]' +
|
308
|
+
param2.short_name + '[/b] have changed\n')
|
309
|
+
# array index starts with 0 -> param[0] is 1. service parameter
|
310
|
+
|
311
|
+
if table['Property']:
|
312
|
+
information.append(infotext)
|
313
|
+
information.append(table)
|
314
|
+
changed_params += 'positive response parameter \'' + param2.short_name + '\',\n'
|
315
|
+
else:
|
316
|
+
changed_params += 'positive response parameter list, '
|
317
|
+
# infotext
|
318
|
+
information.append('List of positive response parameters for service ' +
|
319
|
+
service2.short_name + ' is not identical.')
|
320
|
+
# table
|
321
|
+
information.append({
|
322
|
+
'List': ['Old list', 'New list'],
|
323
|
+
'Values': [str(response1.parameters),
|
324
|
+
str(response2.parameters)]
|
325
|
+
})
|
326
|
+
else:
|
327
|
+
changed_params += 'positive responses list, '
|
328
|
+
# infotext
|
329
|
+
information.append('List of positive responses for service ' + service2.short_name +
|
330
|
+
' is not identical.')
|
331
|
+
# table
|
332
|
+
information.append({
|
333
|
+
'List': ['Old list', 'New list'],
|
334
|
+
'Values': [str(service1.positive_responses),
|
335
|
+
str(service2.positive_responses)]
|
336
|
+
})
|
337
|
+
|
338
|
+
# Negative Responses
|
339
|
+
if len(service1.negative_responses) == len(service2.negative_responses):
|
340
|
+
for res1_idx, response1 in enumerate(service1.negative_responses):
|
341
|
+
for res2_idx, response2 in enumerate(service2.negative_responses):
|
342
|
+
if res1_idx == res2_idx:
|
343
|
+
if len(response1.parameters) == len(response2.parameters):
|
344
|
+
for param1_idx, param1 in enumerate(response1.parameters):
|
345
|
+
for param2_idx, param2 in enumerate(response2.parameters):
|
346
|
+
if param1_idx == param2_idx:
|
347
|
+
# find changed negative response parameter properties
|
348
|
+
table = self.compare_parameters(param1, param2)
|
349
|
+
infotext = (' Properties of ' + str(param1_idx + 1) +
|
350
|
+
'. negative response parameter [b]' +
|
351
|
+
param2.short_name + '[/b] have changed\n')
|
352
|
+
# array index starts with 0 -> param[0] is 1. service parameter
|
353
|
+
|
354
|
+
if table['Property']:
|
355
|
+
information.append(infotext)
|
356
|
+
information.append(table)
|
357
|
+
changed_params += 'negative response parameter \'' + param2.short_name + '\',\n'
|
358
|
+
else:
|
359
|
+
changed_params += 'positive response parameter list, '
|
360
|
+
# infotext
|
361
|
+
information.append('List of positive response parameters for service ' +
|
362
|
+
service2.short_name + ' is not identical.\n')
|
363
|
+
# table
|
364
|
+
information.append({
|
365
|
+
'List': ['Old list', 'New list'],
|
366
|
+
'Values': [str(response1.parameters),
|
367
|
+
str(response2.parameters)]
|
368
|
+
})
|
369
|
+
else:
|
370
|
+
changed_params += 'negative responses list, '
|
371
|
+
# infotext
|
372
|
+
information.append('List of positive responses for service ' + service2.short_name +
|
373
|
+
' is not identical.\n')
|
374
|
+
# table
|
375
|
+
information.append({
|
376
|
+
'List': ['Old list', 'New list'],
|
377
|
+
'Values': [str(service1.negative_responses),
|
378
|
+
str(service2.negative_responses)]
|
379
|
+
})
|
380
|
+
|
381
|
+
return [information, changed_params]
|
382
|
+
|
383
|
+
def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> dict:
|
384
|
+
# compares diagnostic services of two diagnostic layers with each other
|
385
|
+
# save changes in dictionary (service_dict)
|
386
|
+
# TODO: add comparison of SingleECUJobs
|
387
|
+
|
388
|
+
new_services: NewServices = []
|
389
|
+
deleted_services: DeletedServices = []
|
390
|
+
renamed_service: RenamedServices = [[],
|
391
|
+
[]] # TODO: implement list of (str, DiagService)-tuples
|
392
|
+
services_with_param_changes: ServicesWithParamChanges = [
|
393
|
+
[], [], []
|
394
|
+
] # TODO: implement list of tuples (str, str, DiagService)-tuples
|
395
|
+
|
396
|
+
service_dict: SpecsServiceDict = {
|
397
|
+
"diag_layer": dl1.short_name,
|
398
|
+
"diag_layer_type": dl1.variant_type.value,
|
399
|
+
# list with added diagnostic services [service1, service2, service3, ...] Type: DiagService
|
400
|
+
"new_services": new_services,
|
401
|
+
# list with deleted diagnostic services [service1, service2, service3, ...] Type: DiagService
|
402
|
+
"deleted_services": deleted_services,
|
403
|
+
# list with diagnostic services where the service name changed [[services], [old service names]]
|
404
|
+
"changed_name_of_service": renamed_service,
|
405
|
+
# list with diagnostic services where the service parameter changed [[services], [changed_parameters], [information_texts]]
|
406
|
+
"changed_parameters_of_service": services_with_param_changes
|
407
|
+
}
|
408
|
+
# service_dict["changed_name_of_service"][{0 = services, 1 = old service names}][i]
|
409
|
+
# service_dict["changed_parameters_of_service"][{0 = services, 1 = changed_parameters, 2 = information_texts}][i]
|
410
|
+
|
411
|
+
# write diagnostic services of each diagnostic layer into a list (type: odxtools.diagservice.DiagService)
|
412
|
+
services_for_dl1 = dl1.services
|
413
|
+
services_for_dl2 = dl2.services
|
414
|
+
|
415
|
+
# write request parameters (hex string) refering to Diag-Services into a list (type: bytearray)
|
416
|
+
requests_for_dl1: List[Union[bytes, str]] = []
|
417
|
+
requests_for_dl2: List[Union[bytes, str]] = []
|
418
|
+
|
419
|
+
# method service() fails when request parameters are not coded or have a default value
|
420
|
+
# Error message: odxtools.exceptions.OdxError: The parameters {'foo'} are required but missing!
|
421
|
+
for service in services_for_dl1:
|
422
|
+
assert isinstance(service, DiagService)
|
423
|
+
if not getattr(service.request, "required_parameters", None):
|
424
|
+
requests_for_dl1.append(service())
|
425
|
+
else:
|
426
|
+
requests_for_dl1.append(service.short_name)
|
427
|
+
|
428
|
+
for service in services_for_dl2:
|
429
|
+
assert isinstance(service, DiagService)
|
430
|
+
if not getattr(service.request, "required_parameters", None):
|
431
|
+
requests_for_dl2.append(service())
|
432
|
+
else:
|
433
|
+
requests_for_dl2.append(service.short_name)
|
434
|
+
|
435
|
+
# compare diagnostic services
|
436
|
+
for service1_idx, service1 in enumerate(services_for_dl1):
|
437
|
+
|
438
|
+
# check for added diagnostic services
|
439
|
+
if (service1.short_name not in [service.short_name for service in services_for_dl2] and
|
440
|
+
requests_for_dl1[service1_idx] not in requests_for_dl2):
|
441
|
+
|
442
|
+
service_dict["new_services"].append(service1) # type: ignore[union-attr, arg-type]
|
443
|
+
|
444
|
+
# check whether names of diagnostic services have changed
|
445
|
+
elif (service1.short_name not in [service.short_name for service in services_for_dl2]
|
446
|
+
and requests_for_dl1[service1_idx] in requests_for_dl2):
|
447
|
+
|
448
|
+
# get related diagnostic service for request
|
449
|
+
decoded_message = dl2.decode(service1())
|
450
|
+
|
451
|
+
# save information about changes in dictionary
|
452
|
+
|
453
|
+
# add new service (type: DiagService)
|
454
|
+
service_dict["changed_name_of_service"][0].append( # type: ignore[union-attr]
|
455
|
+
service1)
|
456
|
+
# add old service name (type: String)
|
457
|
+
service_dict["changed_name_of_service"][1].append( # type: ignore[union-attr]
|
458
|
+
decoded_message[0].service.short_name)
|
459
|
+
|
460
|
+
# compare request, pos. response and neg. response parameters of diagnostic services
|
461
|
+
detailed_information = self.compare_services(service1, decoded_message[0].service)
|
462
|
+
# detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params]
|
463
|
+
|
464
|
+
# add information about changed diagnostic service parameters to dicitionary
|
465
|
+
if detailed_information[1]: # check whether string 'changed_params' is empty
|
466
|
+
# new service (type: DiagService)
|
467
|
+
service_dict["changed_parameters_of_service"][
|
468
|
+
0].append( # type: ignore[union-attr]
|
469
|
+
service1)
|
470
|
+
# add parameters which have been changed (type: String)
|
471
|
+
service_dict["changed_parameters_of_service"][
|
472
|
+
1].append( # type: ignore[union-attr]
|
473
|
+
detailed_information[1])
|
474
|
+
# add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...]
|
475
|
+
service_dict["changed_parameters_of_service"][
|
476
|
+
2].append( # type: ignore[union-attr]
|
477
|
+
detailed_information[0])
|
478
|
+
|
479
|
+
for service2_idx, service2 in enumerate(services_for_dl2):
|
480
|
+
|
481
|
+
# check for deleted diagnostic services
|
482
|
+
if (service2.short_name not in [service.short_name for service in services_for_dl1]
|
483
|
+
and requests_for_dl2[service2_idx] not in requests_for_dl1 and
|
484
|
+
service2 not in service_dict["deleted_services"]):
|
485
|
+
|
486
|
+
service_dict["deleted_services"].append( # type: ignore[union-attr]
|
487
|
+
service2) # type: ignore[arg-type]
|
488
|
+
|
489
|
+
if service1.short_name == service2.short_name:
|
490
|
+
# compare request, pos. response and neg. response parameters of both diagnostic services
|
491
|
+
detailed_information = self.compare_services(service1, service2)
|
492
|
+
# detailed_information = [[infotext1, table1, infotext2, table2, ...], changed_params]
|
493
|
+
|
494
|
+
# add information about changed diagnostic service parameters to dicitionary
|
495
|
+
if detailed_information[1]: # check whether string 'changed_params' is empty
|
496
|
+
# new service (type: DiagService)
|
497
|
+
service_dict["changed_parameters_of_service"][
|
498
|
+
0].append( # type: ignore[union-attr]
|
499
|
+
service1)
|
500
|
+
# add parameters which have been changed (type: String)
|
501
|
+
service_dict["changed_parameters_of_service"][ # type: ignore[union-attr]
|
502
|
+
1].append(detailed_information[1])
|
503
|
+
# add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...]
|
504
|
+
service_dict["changed_parameters_of_service"][ # type: ignore[union-attr]
|
505
|
+
2].append(detailed_information[0])
|
506
|
+
return service_dict
|
507
|
+
|
508
|
+
def compare_databases(self, database_new: Database, database_old: Database) -> dict:
|
509
|
+
# compares two PDX-files with each other
|
510
|
+
|
511
|
+
new_variants: NewVariants = []
|
512
|
+
deleted_variants: DeletedVariants = []
|
513
|
+
|
514
|
+
changes_variants: SpecsChangesVariants = {
|
515
|
+
"new_diagnostic_layers": new_variants,
|
516
|
+
"deleted_diagnostic_layers": deleted_variants
|
517
|
+
}
|
518
|
+
|
519
|
+
# compare databases
|
520
|
+
for _, dl1 in enumerate(database_new.diag_layers):
|
521
|
+
# check for new diagnostic layers
|
522
|
+
if dl1.short_name not in [dl.short_name for dl in database_old.diag_layers]:
|
523
|
+
changes_variants["new_diagnostic_layers"].append(dl1) # type: ignore[union-attr]
|
524
|
+
|
525
|
+
for _, dl2 in enumerate(database_old.diag_layers):
|
526
|
+
# check for deleted diagnostic layers
|
527
|
+
if (dl2.short_name not in [dl.short_name for dl in database_new.diag_layers] and
|
528
|
+
dl2 not in changes_variants["deleted_diagnostic_layers"]):
|
529
|
+
|
530
|
+
changes_variants[
|
531
|
+
"deleted_diagnostic_layers"].append( # type: ignore[union-attr]
|
532
|
+
dl2)
|
533
|
+
|
534
|
+
if dl1.short_name == dl2.short_name and dl1.short_name in self.diagnostic_layer_names:
|
535
|
+
# compare diagnostic services of both diagnostic layers
|
536
|
+
# save diagnostic service changes in dictionary (empty if no changes)
|
537
|
+
service_dict: SpecsServiceDict = self.compare_diagnostic_layers(dl1, dl2)
|
538
|
+
if service_dict:
|
539
|
+
# adds information about diagnostic service changes to return variable (changes_variants)
|
540
|
+
changes_variants.update({dl1.short_name: service_dict})
|
541
|
+
|
542
|
+
return changes_variants
|
543
|
+
|
544
|
+
|
545
|
+
def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
|
546
|
+
parser = subparsers.add_parser(
|
547
|
+
"compare",
|
548
|
+
description="\n".join([
|
549
|
+
"Compares two versions of diagnostic layers or databases with each other. Checks whether diagnostic services and its parameters have changed.",
|
550
|
+
"",
|
551
|
+
"Examples:",
|
552
|
+
" Comparison of two diagnostic layers:",
|
553
|
+
" odxtools compare ./path/to/database.pdx -v variant1 variant2",
|
554
|
+
" Comparison of two database versions:",
|
555
|
+
" odxtools compare ./path/to/database.pdx -db ./path/to/old-database.pdx",
|
556
|
+
" For more information use:",
|
557
|
+
" odxtools compare -h",
|
558
|
+
]),
|
559
|
+
help="Compares two versions of diagnostic layers and/or databases with each other. Checks whether diagnostic services and its parameters have changed.",
|
560
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
561
|
+
)
|
562
|
+
_parser_utils.add_pdx_argument(parser)
|
563
|
+
|
564
|
+
parser.add_argument(
|
565
|
+
"-v",
|
566
|
+
"--variants",
|
567
|
+
nargs="+",
|
568
|
+
metavar="VARIANT",
|
569
|
+
required=False,
|
570
|
+
default=None,
|
571
|
+
help="Compare specified (ecu) variants to each other.",
|
572
|
+
)
|
573
|
+
|
574
|
+
parser.add_argument(
|
575
|
+
"-db",
|
576
|
+
"--database",
|
577
|
+
nargs="+",
|
578
|
+
default=None,
|
579
|
+
metavar="DATABASE",
|
580
|
+
required=False,
|
581
|
+
help="Compare specified database file(s) to database file of first input argument.",
|
582
|
+
)
|
583
|
+
|
584
|
+
parser.add_argument(
|
585
|
+
"-nd",
|
586
|
+
"--no-details",
|
587
|
+
action="store_false",
|
588
|
+
default=True,
|
589
|
+
required=False,
|
590
|
+
help="Don't show all service parameter details",
|
591
|
+
)
|
592
|
+
|
593
|
+
parser.add_argument(
|
594
|
+
"-po",
|
595
|
+
"--plumbing-output",
|
596
|
+
action="store_true",
|
597
|
+
required=False,
|
598
|
+
help="Print full objects instead of selected and formatted attributes",
|
599
|
+
)
|
600
|
+
# TODO
|
601
|
+
# Idea: provide folder with multiple pdx files as argument
|
602
|
+
# -> load all pdx files in folder, sort them alphabetically, compare databases pairwaise
|
603
|
+
# -> calculate metrics (number of added services, number of changed services, number of removed services, total number of services per ecu variant, ...)
|
604
|
+
# -> display metrics graphically
|
605
|
+
|
606
|
+
|
607
|
+
def run(args: argparse.Namespace) -> None:
|
608
|
+
|
609
|
+
task = Comparison()
|
610
|
+
task.param_detailed = args.no_details
|
611
|
+
task.obj_detailed = args.plumbing_output
|
612
|
+
|
613
|
+
db_names = [args.pdx_file if isinstance(args.pdx_file, str) else str(args.pdx_file[0])]
|
614
|
+
|
615
|
+
if args.database and args.variants:
|
616
|
+
# compare specified databases, consider only specified variants
|
617
|
+
|
618
|
+
for name in args.database:
|
619
|
+
db_names.append(name) if isinstance(name, str) else str(name[0])
|
620
|
+
|
621
|
+
task.databases = [load_file(name) for name in db_names]
|
622
|
+
diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers}
|
623
|
+
|
624
|
+
task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants))
|
625
|
+
|
626
|
+
for name in args.variants:
|
627
|
+
if name not in task.diagnostic_layer_names:
|
628
|
+
print(f"The variant '{name}' could not be found!")
|
629
|
+
return
|
630
|
+
|
631
|
+
task.db_indicator_1 = 0
|
632
|
+
|
633
|
+
for db_idx, _ in enumerate(task.databases):
|
634
|
+
if db_idx + 1 >= len(task.databases):
|
635
|
+
break
|
636
|
+
task.db_indicator_2 = db_idx + 1
|
637
|
+
|
638
|
+
print("\n")
|
639
|
+
print("Changes in file [b]" + os.path.basename(db_names[0]) + "[/b]\n (compared to " +
|
640
|
+
os.path.basename(db_names[db_idx + 1]) + ")")
|
641
|
+
|
642
|
+
print("\n")
|
643
|
+
print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})")
|
644
|
+
print_dl_metrics([
|
645
|
+
variant for variant in task.databases[0].diag_layers
|
646
|
+
if variant.short_name in task.diagnostic_layer_names
|
647
|
+
])
|
648
|
+
|
649
|
+
print("\n")
|
650
|
+
print(f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})")
|
651
|
+
print_dl_metrics([
|
652
|
+
variant for variant in task.databases[db_idx + 1].diag_layers
|
653
|
+
if variant.short_name in task.diagnostic_layer_names
|
654
|
+
])
|
655
|
+
|
656
|
+
task.print_database_changes(
|
657
|
+
task.compare_databases(task.databases[0], task.databases[db_idx + 1]))
|
658
|
+
|
659
|
+
elif args.database:
|
660
|
+
# compare specified databases, consider all variants
|
661
|
+
|
662
|
+
for name in args.database:
|
663
|
+
db_names.append(name)
|
664
|
+
task.databases = [load_file(name) for name in db_names]
|
665
|
+
|
666
|
+
# collect all diagnostic layers from all specified databases
|
667
|
+
task.diagnostic_layer_names = {
|
668
|
+
dl.short_name
|
669
|
+
for db in task.databases
|
670
|
+
for dl in db.diag_layers
|
671
|
+
}
|
672
|
+
task.db_indicator_1 = 0
|
673
|
+
|
674
|
+
for db_idx, _ in enumerate(task.databases):
|
675
|
+
if db_idx + 1 >= len(task.databases):
|
676
|
+
break
|
677
|
+
task.db_indicator_2 = db_idx + 1
|
678
|
+
|
679
|
+
print("\n")
|
680
|
+
print("Changes in file [b]" + os.path.basename(db_names[0]) + "[/b]\n (compared to " +
|
681
|
+
os.path.basename(db_names[db_idx + 1]) + ")")
|
682
|
+
|
683
|
+
print("\n")
|
684
|
+
print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})")
|
685
|
+
print_dl_metrics(list(task.databases[0].diag_layers))
|
686
|
+
|
687
|
+
print("\n")
|
688
|
+
print(f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})")
|
689
|
+
print_dl_metrics(list(task.databases[db_idx + 1].diag_layers))
|
690
|
+
|
691
|
+
task.print_database_changes(
|
692
|
+
task.compare_databases(task.databases[0], task.databases[db_idx + 1]))
|
693
|
+
|
694
|
+
elif args.variants:
|
695
|
+
# no databases specified -> comparison of diagnostic layers
|
696
|
+
|
697
|
+
odxdb = _parser_utils.load_file(args)
|
698
|
+
task.databases = [odxdb]
|
699
|
+
|
700
|
+
diag_layer_names = {dl.short_name for db in task.databases for dl in db.diag_layers}
|
701
|
+
|
702
|
+
task.diagnostic_layer_names = diag_layer_names.intersection(set(args.variants))
|
703
|
+
task.diagnostic_layers = [
|
704
|
+
dl for db in task.databases for dl in db.diag_layers
|
705
|
+
if dl.short_name in task.diagnostic_layer_names
|
706
|
+
]
|
707
|
+
|
708
|
+
for name in args.variants:
|
709
|
+
if name not in task.diagnostic_layer_names:
|
710
|
+
print(f"The variant '{name}' could not be found!")
|
711
|
+
return
|
712
|
+
|
713
|
+
print("\n")
|
714
|
+
print(f"Overview of diagnostic layers: ")
|
715
|
+
print_dl_metrics(task.diagnostic_layers)
|
716
|
+
|
717
|
+
for db_idx, dl in enumerate(task.diagnostic_layers):
|
718
|
+
if db_idx + 1 >= len(task.diagnostic_layers):
|
719
|
+
break
|
720
|
+
|
721
|
+
print("\n")
|
722
|
+
print(
|
723
|
+
f"Changes in diagnostic layer [b]{dl.short_name}[/b] ({dl.variant_type.value})\n (compared to {task.diagnostic_layers[db_idx + 1].short_name} ({task.diagnostic_layers[db_idx + 1].variant_type.value}))"
|
724
|
+
)
|
725
|
+
task.print_dl_changes(
|
726
|
+
task.compare_diagnostic_layers(dl, task.diagnostic_layers[db_idx + 1]))
|
727
|
+
|
728
|
+
else:
|
729
|
+
# no databases & no variants specified
|
730
|
+
print("Please specify either a database or variant for a comparison")
|