odxtools 6.4.3__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.
@@ -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")