Coverage for pyEDAA/OutputFilter/Xilinx/Common2.py: 88%
332 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-16 06:19 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-16 06:19 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ___ _ _ _____ _ _ _ #
3# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2025 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""Basic classes for outputs from AMD/Xilinx Vivado."""
32from datetime import datetime
33from re import Pattern, compile as re_compile
34from typing import ClassVar, Optional as Nullable, Generator, List, Dict, Tuple, Type
36from pyTooling.Decorators import export, readonly
37from pyTooling.MetaClasses import ExtendedType
38from pyTooling.Versioning import YearReleaseVersion
40from pyEDAA.OutputFilter.Xilinx import Line, LineKind, VivadoMessage
41from pyEDAA.OutputFilter.Xilinx import VivadoInfoMessage, VivadoWarningMessage, VivadoCriticalWarningMessage, VivadoErrorMessage
42from pyEDAA.OutputFilter.Xilinx.Exception import ProcessorException
45@export
46class VivadoMessagesMixin(metaclass=ExtendedType, mixin=True):
47 _infoMessages: List[VivadoInfoMessage]
48 _warningMessages: List[VivadoWarningMessage]
49 _criticalWarningMessages: List[VivadoCriticalWarningMessage]
50 _errorMessages: List[VivadoErrorMessage]
51 _toolIDs: Dict[int, str]
52 _toolNames: Dict[str, int]
53 _messagesByID: Dict[int, Dict[int, List[VivadoMessage]]]
55 def __init__(self) -> None:
56 self._infoMessages = []
57 self._warningMessages = []
58 self._criticalWarningMessages = []
59 self._errorMessages = []
60 self._toolIDs = {}
61 self._toolNames = {}
62 self._messagesByID = {}
64 @readonly
65 def ToolIDs(self) -> Dict[int, str]:
66 return self._toolIDs
68 @readonly
69 def ToolNames(self) -> Dict[str, int]:
70 return self._toolNames
72 @readonly
73 def MessagesByID(self) -> Dict[int, Dict[int, List[VivadoMessage]]]:
74 return self._messagesByID
76 @readonly
77 def InfoMessages(self) -> List[VivadoInfoMessage]:
78 return self._infoMessages
80 @readonly
81 def WarningMessages(self) -> List[VivadoWarningMessage]:
82 return self._warningMessages
84 @readonly
85 def CriticalWarningMessages(self) -> List[VivadoCriticalWarningMessage]:
86 return self._criticalWarningMessages
88 @readonly
89 def ErrorMessages(self) -> List[VivadoErrorMessage]:
90 return self._errorMessages
92 def _AddMessage(self, message: VivadoMessage) -> None:
93 if isinstance(message, VivadoInfoMessage):
94 self._infoMessages.append(message)
95 elif isinstance(message, VivadoWarningMessage):
96 self._warningMessages.append(message)
97 elif isinstance(message, VivadoCriticalWarningMessage): 97 ↛ 99line 97 didn't jump to line 99 because the condition on line 97 was always true
98 self._criticalWarningMessages.append(message)
99 elif isinstance(message, VivadoErrorMessage):
100 self._errorMessages.append(message)
102 if message._toolID in self._messagesByID:
103 sub = self._messagesByID[message._toolID]
104 if message._messageKindID in sub:
105 sub[message._messageKindID].append(message)
106 else:
107 sub[message._messageKindID] = [message]
108 else:
109 self._toolIDs[message._toolID] = message._toolName
110 self._toolNames[message._toolName] = message._toolID
111 self._messagesByID[message._toolID] = {message._messageKindID: [message]}
114@export
115class BaseParser(VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
116 def __init__(self) -> None:
117 super().__init__()
120@export
121class Parser(BaseParser):
122 _processor: "Processor"
124 def __init__(self, processor: "Processor"):
125 super().__init__()
127 self._processor = processor
129 @readonly
130 def Processor(self) -> "Processor":
131 return self._processor
134@export
135class Preamble(Parser):
136 _toolVersion: Nullable[YearReleaseVersion]
137 _startDatetime: Nullable[datetime]
139 _VERSION: ClassVar[Pattern] = re_compile(r"""# Vivado v(\d+\.\d(\.\d)?) \(64-bit\)""")
140 _STARTTIME: ClassVar[Pattern] = re_compile(r"""# Start of session at: (\w+ \w+ \d+ \d+:\d+:\d+ \d+)""")
142 def __init__(self, processor: "BaseProcessor"):
143 super().__init__(processor)
145 self._toolVersion = None
146 self._startDatetime = None
148 @readonly
149 def ToolVersion(self) -> YearReleaseVersion:
150 return self._toolVersion
152 @readonly
153 def StartDatetime(self) -> datetime:
154 return self._startDatetime
156 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
157 if line.StartsWith("#----"): 157 ↛ 160line 157 didn't jump to line 160 because the condition on line 157 was always true
158 line._kind = LineKind.SectionDelimiter
159 else:
160 line._kind |= LineKind.ProcessorError
162 line = yield line
164 while True:
165 if (match := self._VERSION.match(line._message)) is not None:
166 self._toolVersion = YearReleaseVersion.Parse(match[1])
167 line._kind = LineKind.Normal
168 elif (match := self._STARTTIME.match(line._message)) is not None:
169 self._startDatetime = datetime.strptime(match[1], "%a %b %d %H:%M:%S %Y")
170 line._kind = LineKind.Normal
171 elif line.StartsWith("#----"):
172 line._kind = LineKind.SectionDelimiter
173 break
174 else:
175 line._kind = LineKind.Verbose
177 line = yield line
179 nextLine = yield line
180 return nextLine
182@export
183class Task(BaseParser, VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
184 # _START: ClassVar[str]
185 # _FINISH: ClassVar[str]
186 _TIME: ClassVar[str] = "Time (s):"
188 _PARSERS: ClassVar[Tuple[Type["Phase"], ...]] = tuple()
190 _command: "Command"
191 _duration: float
192 _phases: Dict[Type["Phase"], "Phase"]
194 def __init__(self, command: "Command"):
195 super().__init__()
196 VivadoMessagesMixin.__init__(self)
198 self._command = command
199 self._phases = {p: p(self) for p in self._PARSERS}
201 @readonly
202 def Command(self) -> "Command":
203 return self._command
205 @readonly
206 def Phases(self) -> Dict[Type["Phase"], "Phase"]:
207 return self._phases
209 def __getitem__(self, key: Type["Phase"]) -> "Phase":
210 return self._phases[key]
212 def _TaskStart(self, line: Line) -> Generator[Line, Line, Line]:
213 if not line.StartsWith(self._START): 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true
214 raise ProcessorException()
216 line._kind = LineKind.TaskStart
217 nextLine = yield line
218 return nextLine
220 def _TaskFinish(self, line: Line) -> Generator[Line, Line, Line]:
221 if not line.StartsWith(self._FINISH): 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true
222 raise ProcessorException()
224 line._kind = LineKind.TaskEnd
225 line = yield line
226 while self._TIME is not None: 226 ↛ 233line 226 didn't jump to line 233 because the condition on line 226 was always true
227 if line.StartsWith(self._TIME):
228 line._kind = LineKind.TaskTime
229 break
231 line = yield line
233 line = yield line
234 return line
236 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
237 line = yield from self._TaskStart(line)
239 while True:
240 if line._kind is LineKind.Empty:
241 line = yield line
242 continue
243 elif self._FINISH is not None and line.StartsWith("Ending"): 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true
244 break
245 elif isinstance(line, VivadoMessage): 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 self._AddMessage(line)
247 elif line.StartsWith(self._TIME): 247 ↛ 252line 247 didn't jump to line 252 because the condition on line 247 was always true
248 line._kind = LineKind.TaskTime
249 nextLine = yield line
250 return nextLine
252 line = yield line
254 nextLine = yield from self._TaskFinish(line)
255 return nextLine
257 def __str__(self) -> str:
258 return self._NAME
261@export
262class Phase(BaseParser, VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
263 # _START: ClassVar[str]
264 # _FINISH: ClassVar[str]
265 _TIME: ClassVar[str] = "Time (s):"
267 _task: Task
268 _duration: float
270 def __init__(self, task: Task):
271 super().__init__()
272 VivadoMessagesMixin.__init__(self)
274 self._task = task
276 @readonly
277 def Task(self) -> Task:
278 return self._task
280 def _PhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
281 if not line.StartsWith(self._START): 281 ↛ 282line 281 didn't jump to line 282 because the condition on line 281 was never true
282 raise ProcessorException()
284 line._kind = LineKind.PhaseStart
285 nextLine = yield line
286 return nextLine
288 def _PhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
289 if not line.StartsWith(self._FINISH): 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true
290 raise ProcessorException()
292 line._kind = LineKind.PhaseEnd
293 line = yield line
295 if self._TIME is not None: 295 ↛ 305line 295 didn't jump to line 305 because the condition on line 295 was always true
296 while self._TIME is not None: 296 ↛ 303line 296 didn't jump to line 303 because the condition on line 296 was always true
297 if line.StartsWith(self._TIME):
298 line._kind = LineKind.PhaseTime
299 break
301 line = yield line
303 line = yield line
305 return line
307 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
308 line = yield from self._PhaseStart(line)
310 while True:
311 if line._kind is LineKind.Empty:
312 line = yield line
313 continue
314 elif isinstance(line, VivadoMessage):
315 self._AddMessage(line)
316 elif line.StartsWith(self._FINISH):
317 break
319 line = yield line
321 nextLine = yield from self._PhaseFinish(line)
322 return nextLine
324@export
325class SubPhase(BaseParser, VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
326 # _START: ClassVar[str]
327 # _FINISH: ClassVar[str]
329 _phase: Phase
330 _duration: float
332 def __init__(self, phase: Phase):
333 super().__init__()
334 VivadoMessagesMixin.__init__(self)
336 self._phase = phase
338 def _SubPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
339 if not line.StartsWith(self._START): 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true
340 raise ProcessorException()
342 line._kind = LineKind.SubPhaseStart
343 nextLine = yield line
344 return nextLine
346 def _SubPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
347 if not line.StartsWith(self._FINISH): 347 ↛ 348line 347 didn't jump to line 348 because the condition on line 347 was never true
348 raise ProcessorException()
350 if self._TIME is None:
351 line._kind = LineKind.SubPhaseTime
352 else:
353 line._kind = LineKind.SubPhaseEnd
355 line = yield line
356 while self._TIME is not None: 356 ↛ 363line 356 didn't jump to line 363 because the condition on line 356 was always true
357 if line.StartsWith(self._TIME):
358 line._kind = LineKind.SubPhaseTime
359 break
361 line = yield line
363 nextLine = yield line
364 return nextLine
366 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
367 line = yield from self._SubPhaseStart(line)
369 while True:
370 if line._kind is LineKind.Empty:
371 line = yield line
372 continue
373 elif line.StartsWith(self._FINISH):
374 break
375 elif isinstance(line, VivadoMessage):
376 self._AddMessage(line)
378 line = yield line
380 nextLine = yield from self._SubPhaseFinish(line)
381 return nextLine
384@export
385class SubSubPhase(BaseParser, VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
386 # _START: ClassVar[str]
387 # _FINISH: ClassVar[str]
389 _subphase: SubPhase
390 _duration: float
392 def __init__(self, subphase: SubPhase):
393 super().__init__()
394 VivadoMessagesMixin.__init__(self)
396 self._subphase = subphase
398 def _SubSubPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
399 if not line.StartsWith(self._START): 399 ↛ 400line 399 didn't jump to line 400 because the condition on line 399 was never true
400 raise ProcessorException()
402 line._kind = LineKind.SubSubPhaseStart
403 nextLine = yield line
404 return nextLine
406 def _SubSubPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
407 if not line.StartsWith(self._FINISH): 407 ↛ 408line 407 didn't jump to line 408 because the condition on line 407 was never true
408 raise ProcessorException()
410 line._kind = LineKind.SubSubPhaseEnd
411 line = yield line
413 while self._TIME is not None: 413 ↛ 420line 413 didn't jump to line 420 because the condition on line 413 was always true
414 if line.StartsWith(self._TIME):
415 line._kind = LineKind.SubSubPhaseTime
416 break
418 line = yield line
420 nextLine = yield line
421 return nextLine
423 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
424 line = yield from self._SubSubPhaseStart(line)
426 while True:
427 if line._kind is LineKind.Empty:
428 line = yield line
429 continue
430 elif line.StartsWith(self._FINISH):
431 break
432 elif isinstance(line, VivadoMessage):
433 self._AddMessage(line)
435 line = yield line
437 nextLine = yield from self._SubSubPhaseFinish(line)
438 return nextLine
441@export
442class SubSubSubPhase(BaseParser, VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
443 # _START: ClassVar[str]
444 # _FINISH: ClassVar[str]
446 _subsubphase: SubSubPhase
447 _duration: float
449 def __init__(self, subsubphase: SubSubPhase):
450 super().__init__()
451 VivadoMessagesMixin.__init__(self)
453 self._subsubphase = subsubphase
455 def _SubSubSubPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
456 if not line.StartsWith(self._START): 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true
457 raise ProcessorException()
459 line._kind = LineKind.SubSubSubPhaseStart
460 nextLine = yield line
461 return nextLine
463 def _SubSubSubPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
464 if not line.StartsWith(self._FINISH): 464 ↛ 465line 464 didn't jump to line 465 because the condition on line 464 was never true
465 raise ProcessorException()
467 line._kind = LineKind.SubSubSubPhaseEnd
468 line = yield line
470 while self._TIME is not None: 470 ↛ 477line 470 didn't jump to line 477 because the condition on line 470 was always true
471 if line.StartsWith(self._TIME):
472 line._kind = LineKind.SubSubSubPhaseTime
473 break
475 line = yield line
477 nextLine = yield line
478 return nextLine
480 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
481 line = yield from self._SubSubSubPhaseStart(line)
483 while True:
484 if line._kind is LineKind.Empty:
485 line = yield line
486 continue
487 elif line.StartsWith(self._FINISH):
488 break
489 elif isinstance(line, VivadoMessage):
490 self._AddMessage(line)
492 line = yield line
494 nextLine = yield from self._SubSubSubPhaseFinish(line)
495 return nextLine