Coverage for pyEDAA/OutputFilter/Xilinx/Common2.py: 78%
1054 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 22:59 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 22:59 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ___ _ _ _____ _ _ _ #
3# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2026 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, Any
36from pyTooling.Common import getFullyQualifiedName
37from pyTooling.Decorators import export, readonly
38from pyTooling.MetaClasses import ExtendedType
39from pyTooling.Versioning import YearReleaseVersion
40from pyTooling.Warning import WarningCollector, Warning, CriticalWarning
42from pyEDAA.OutputFilter.Xilinx import Line, LineKind, VivadoMessage, InfoMessage, WarningMessage, CriticalWarningMessage, ErrorMessage
43from pyEDAA.OutputFilter.Xilinx import VivadoInfoMessage, VivadoWarningMessage, VivadoCriticalWarningMessage, VivadoErrorMessage
44from pyEDAA.OutputFilter.Xilinx.Exception import ProcessorException, NotPresentException
46MAJOR = r"(?P<major>\d+)"
47MAJOR_MINOR = r"(?P<major>\d+)\.(?P<minor>\d+)"
48MAJOR_MINOR_MICRO = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<micro>\d+)"
49MAJOR_MINOR_MICRO_NANO = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<micro>\d+)\.(?P<nano>\d+)"
52@export
53class UndetectedEnd(CriticalWarning):
54 _line: Line
56 def __init__(self, message: str, line: Line) -> None:
57 super().__init__(message)
59 self._line = line
61 @readonly
62 def Line(self) -> Line:
63 return self._line
66@export
67class UnknownLine(Warning):
68 _line: Line
70 def __init__(self, message: str, line: Line) -> None:
71 super().__init__(message)
73 self._line = line
75 @readonly
76 def Line(self) -> Line:
77 return self._line
80@export
81class UnknownTask(UnknownLine):
82 pass
85@export
86class UnknownSubTask(UnknownLine):
87 pass
90@export
91class UnknownSection(UnknownLine):
92 pass
95@export
96class UnknownPhase(UnknownLine):
97 pass
100@export
101class UnknownSubPhase(UnknownLine):
102 pass
105@export
106class SubTaskNotPresentException(NotPresentException):
107 pass
110@export
111class PhaseNotPresentException(NotPresentException):
112 pass
115@export
116class NestedTaskNotPresentException(NotPresentException):
117 pass
120@export
121class VivadoMessagesMixin(metaclass=ExtendedType, mixin=True):
122 _infoMessages: List[VivadoInfoMessage]
123 _warningMessages: List[VivadoWarningMessage]
124 _criticalWarningMessages: List[VivadoCriticalWarningMessage]
125 _errorMessages: List[VivadoErrorMessage]
126 _toolIDs: Dict[int, str]
127 _toolNames: Dict[str, int]
128 _messagesByID: Dict[int, Dict[int, List[VivadoMessage]]]
130 def __init__(self) -> None:
131 self._infoMessages = []
132 self._warningMessages = []
133 self._criticalWarningMessages = []
134 self._errorMessages = []
135 self._toolIDs = {}
136 self._toolNames = {}
137 self._messagesByID = {}
139 @readonly
140 def ToolIDs(self) -> Dict[int, str]:
141 return self._toolIDs
143 @readonly
144 def ToolNames(self) -> Dict[str, int]:
145 return self._toolNames
147 @readonly
148 def MessagesByID(self) -> Dict[int, Dict[int, List[VivadoMessage]]]:
149 return self._messagesByID
151 @readonly
152 def InfoMessages(self) -> List[VivadoInfoMessage]:
153 return self._infoMessages
155 @readonly
156 def WarningMessages(self) -> List[VivadoWarningMessage]:
157 return self._warningMessages
159 @readonly
160 def CriticalWarningMessages(self) -> List[VivadoCriticalWarningMessage]:
161 return self._criticalWarningMessages
163 @readonly
164 def ErrorMessages(self) -> List[VivadoErrorMessage]:
165 return self._errorMessages
167 def _AddMessage(self, message: VivadoMessage) -> None:
168 if isinstance(message, InfoMessage):
169 self._infoMessages.append(message)
170 elif isinstance(message, WarningMessage):
171 self._warningMessages.append(message)
172 elif isinstance(message, CriticalWarningMessage):
173 self._criticalWarningMessages.append(message)
174 elif isinstance(message, ErrorMessage): 174 ↛ 177line 174 didn't jump to line 177 because the condition on line 174 was always true
175 self._errorMessages.append(message)
177 if message._toolID in self._messagesByID:
178 sub = self._messagesByID[message._toolID]
179 if message._messageKindID in sub:
180 sub[message._messageKindID].append(message)
181 else:
182 sub[message._messageKindID] = [message]
183 else:
184 if message._toolID is not None:
185 self._toolIDs[message._toolID] = message._toolName
186 self._toolNames[message._toolName] = message._toolID
188 self._messagesByID[message._toolID] = {message._messageKindID: [message]}
191@export
192class BaseParser(VivadoMessagesMixin, metaclass=ExtendedType, slots=True):
193 def __init__(self) -> None:
194 super().__init__()
197@export
198class Parser(BaseParser):
199 _processor: "Processor"
201 def __init__(self, processor: "Processor") -> None:
202 super().__init__()
204 self._processor = processor
206 @readonly
207 def Processor(self) -> "Processor":
208 return self._processor
211@export
212class Preamble(Parser):
213 """
214 A parser for the preamble emitted by Vivado at session start.
216 .. rubric:: Extracted information
218 * Vivado tool version. |br|
219 See :data:`ToolVersion`
220 * Session start timestamp (date and time). |br|
221 See :data:`StartDatetime`
223 .. rubric:: Example
225 .. code-block::
227 #-----------------------------------------------------------
228 # Vivado v2025.1 (64-bit)
229 # SW Build 6140274 on Thu May 22 00:12:29 MDT 2025
230 # IP Build 6138677 on Thu May 22 03:10:11 MDT 2025
231 # SharedData Build 6139179 on Tue May 20 17:58:58 MDT 2025
232 # Start of session at: Thu Jun 12 18:39:05 2025
233 # Process ID : 28856
234 # Current directory : C:/Git/.../StopWatch/project/4_WithTiming.runs/impl_1
235 # Command line : vivado.exe -log toplevel.vdi -applog -product Vivado -messageDb vivado.pb -mode batch -source toplevel.tcl -notrace
236 # Log file : C:/Git/.../StopWatch/project/4_WithTiming.runs/impl_1/toplevel.vdi
237 # Journal file : C:/Git/.../StopWatch/project/4_WithTiming.runs/impl_1\vivado.jou
238 # Running On : Paebbels
239 # Platform : Windows Server 2016 or Windows 10
240 # Operating System : 26100
241 # Processor Detail : 11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz
242 # CPU Frequency : 2611 MHz
243 # CPU Physical cores : 8
244 # CPU Logical cores : 16
245 # Host memory : 34048 MB
246 # Swap memory : 28991 MB
247 # Total Virtual : 63039 MB
248 # Available Virtual : 29246 MB
249 #-----------------------------------------------------------
250 """
251 _VERSION: ClassVar[Pattern] = re_compile(r"""# Vivado v(\d+\.\d(\.\d)?) \(64-bit\)""")
252 _STARTTIME: ClassVar[Pattern] = re_compile(r"""# Start of session at: (\w+\s+\w+\s+\d+\s+\d+:\d+:\d+\s+\d+)""")
254 _toolVersion: Nullable[YearReleaseVersion] #: Used Vivado version.
255 _startDatetime: Nullable[datetime] #: Session start timestamp.
257 def __init__(self, processor: "BaseProcessor") -> None:
258 """
259 Initializes a Vivado preamble parser.
261 :param processor: Reference to the Vivado log processor.
262 """
263 super().__init__(processor)
265 self._toolVersion = None
266 self._startDatetime = None
268 @readonly
269 def ToolVersion(self) -> YearReleaseVersion:
270 """
271 Read-only property to access the extracted Vivado tool version.
273 :returns: The used Vivado version as reported in the Vivado log messages.
274 """
275 return self._toolVersion
277 @readonly
278 def StartDatetime(self) -> datetime:
279 """
280 Read-only property to access the date and time when the Vivado session was started.
282 :returns: Datatime when the session was started.
283 :raises ProcessorException: When start timestamp wasn't extracted from preamble.
284 """
285 if self._startDatetime is None: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 raise ProcessorException("No start timestamp extracted from preamble.")
288 return self._startDatetime
290 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
291 """
292 A generator for processing the Vivado session preamble line-by-line.
294 :param line: First line to process.
295 :returns: A generator processing log messages.
296 """
297 if line.StartsWith("#----"): 297 ↛ 300line 297 didn't jump to line 300 because the condition on line 297 was always true
298 line._kind = LineKind.SectionDelimiter
299 else:
300 line._kind |= LineKind.ProcessorError # TODO: throw / return error
302 line = yield line
304 # a normal preamble has up to 23 lines including both delimiter lines.
305 for _ in range(30): 305 ↛ 320line 305 didn't jump to line 320 because the loop on line 305 didn't complete
306 if (match := self._VERSION.match(line._message)) is not None:
307 self._toolVersion = YearReleaseVersion.Parse(match[1])
308 line._kind = LineKind.Normal
309 elif (match := self._STARTTIME.match(line._message)) is not None:
310 self._startDatetime = datetime.strptime(match[1], "%a %b %d %H:%M:%S %Y")
311 line._kind = LineKind.Normal
312 elif line.StartsWith("#----"):
313 line._kind = LineKind.SectionDelimiter
314 break
315 else:
316 line._kind = LineKind.Verbose
318 line = yield line
319 else:
320 line._kind |= LineKind.ProcessorError # TODO: throw / return error
322 nextLine = yield line
323 return nextLine
326@export
327class Postamble(Parser, VivadoMessagesMixin):
328 """
329 A parser for the postamble emitted by Vivado at session end.
331 .. rubric:: Extracted information
333 * Session exit timestamp (date and time). |br|
334 See :data:`ExitDatetime`
336 .. rubric:: Example
338 .. code-block::
340 INFO: [Common 17-206] Exiting Vivado at Tue Sep 2 08:46:23 2025...
342 """
343 _INFO: Tuple[int, int] = (17, 206)
344 _ENDTIME: ClassVar[Pattern] = re_compile(r"""Exiting Vivado at (\w+\s+\w+\s+\d+\s+\d+:\d+:\d+\s+\d+)""")
346 _exitDatetime: Nullable[datetime] #: Session exit timestamp.
348 def __init__(self, processor: "BaseProcessor") -> None:
349 """
350 Initializes a Vivado postamble parser.
352 :param processor: Reference to the Vivado log processor.
353 """
354 super().__init__(processor)
355 VivadoMessagesMixin.__init__(self)
357 self._exitDatetime = None
359 @readonly
360 def ExitDatetime(self) -> Nullable[datetime]:
361 """
362 Read-only property to access the date and time when the Vivado session was exited.
364 :returns: Datatime when the session was exited.
365 :raises ProcessorException: When exit timestamp wasn't extracted from postamble.
366 """
367 if self._exitDatetime is None: 367 ↛ 368line 367 didn't jump to line 368 because the condition on line 367 was never true
368 raise ProcessorException("No exit timestamp extracted from postamble.")
370 return self._exitDatetime
372 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
373 """
374 A generator for processing the Vivado session preamble line-by-line.
376 :param line: First line to process.
377 :returns: A generator processing log messages.
378 """
379 if isinstance(line, VivadoMessage): 379 ↛ 385line 379 didn't jump to line 385 because the condition on line 379 was always true
380 self._AddMessage(line)
382 if not isinstance(line, VivadoInfoMessage): 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true
383 raise ProcessorException(f"{self.__class__.__name__}.Generator(): Expected '{self._ENDTIME}' at line {line._lineNumber}.")
385 if (match := self._ENDTIME.match(line._message)) is not None:
386 self._exitDatetime = datetime.strptime(match[1], "%a %b %d %H:%M:%S %Y")
387 else:
388 pass
390 line = yield line
392 # todo: should we receive and expect an ned-token like None?
393 return line
396@export
397class Task(BaseParser, VivadoMessagesMixin):
398 """
399 A task's output emitted by a Vivado command.
401 .. rubric:: Extracted information
403 * Vivado messages (info, warning, critical warning, error).
405 .. rubric:: Example
407 .. code-block::
409 Starting Cache Timing Information Task
410 INFO: [Timing 38-35] 79-Done setting XDC timing constraints.
411 Ending Cache Timing Information Task | Checksum: 19fe8cb97
413 Time (s): cpu = 00:00:09 ; elapsed = 00:00:09 . Memory (MB): peak = 1370.594 ; gain = 493.266
415 """
416 # _NAME: ClassVar[str]
417 # _START: ClassVar[str]
418 # _FINISH: ClassVar[str]
419 _TIME: ClassVar[str] = "Time (s):"
421 _command: "Command" #: Reference to the command (parent).
422 _duration: float #: Duration of a task according to reported times by Vivado.
424 def __init__(self, command: "Command") -> None:
425 """
426 Initializes a task (without child elements).
428 :param command: Reference to the command.
429 """
430 super().__init__()
431 VivadoMessagesMixin.__init__(self)
433 self._command = command
435 @readonly
436 def Command(self) -> "Command":
437 """
438 Read-only property to access the command.
440 :returns: The command this task's output was logged for.
441 """
442 return self._command
444 def _TaskStart(self, line: Line) -> Generator[Line, Line, Line]:
445 """
446 A generator for processing a task start (single line).
448 :param line: First line to process (task start).
449 :returns: A generator processing log messages.
450 :raises ProcessorException: If first line doesn't conform to the *task start* pattern.
451 """
452 if not line.StartsWith(self._START): 452 ↛ 453line 452 didn't jump to line 453 because the condition on line 452 was never true
453 raise ProcessorException(f"{self.__class__.__name__}._TaskStart(): Expected '{self._START}' at line {line._lineNumber}.")
455 line._kind = LineKind.TaskStart
456 nextLine = yield line
457 return nextLine
459 def _TaskFinish(self, line: Line) -> Generator[Line, Line, Line]:
460 """
461 A generator for processing a task finish line-by-line.
463 :param line: First line to process (task finish).
464 :returns: A generator processing log messages.
465 :raises ProcessorException: If finish line doesn't conform to the *task finish* pattern.
466 """
467 if not line.StartsWith(self._FINISH): 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true
468 raise ProcessorException(f"{self.__class__.__name__}._TaskFinish(): Expected '{self._FINISH}' at line {line._lineNumber}.")
470 line._kind = LineKind.TaskEnd
471 line = yield line
472 while self._TIME is not None: # TODO: limit search for time pattern to XX lines 472 ↛ 479line 472 didn't jump to line 479 because the condition on line 472 was always true
473 if line.StartsWith(self._TIME):
474 line._kind = LineKind.TaskTime
475 break
477 line = yield line
479 nextLine = yield line
480 return nextLine
482 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
483 """
484 A generator for processing a task without child elements line-by-line.
486 .. rubric:: Algorithm
488 1. Send first line to :meth:`_TaskStart`.
489 2. Process body lines
491 * Collect Vivado messages (info, warning, critical warning, error).
492 * Check for *task finish* pattern.
493 * Check for *time* pattern.
495 3. Send last lines to :meth:`_TaskFinish`.
497 :param line: First line to process.
498 :returns: A generator processing log messages.
499 """
500 line = yield from self._TaskStart(line)
502 while True:
503 if line._kind is LineKind.Empty:
504 line = yield line
505 continue
506 elif self._FINISH is not None and line.StartsWith("Ending"):
507 break
508 elif isinstance(line, VivadoMessage):
509 self._AddMessage(line)
510 elif line.StartsWith(self._TIME):
511 line._kind = LineKind.TaskTime
512 nextLine = yield line
513 return nextLine
515 line = yield line
517 nextLine = yield from self._TaskFinish(line)
518 return nextLine
520 def __str__(self) -> str:
521 return f"{self.__class__.__name__}: {self._START}"
524@export
525class TaskWithSubTasks(Task):
526 """
527 A task's output emitted by a Vivado command.
529 .. rubric:: Extracted information
531 * Vivado messages (info, warning, critical warning, error).
532 * Subtasks
534 .. rubric:: Example
536 .. code-block::
538 Starting Cache Timing Information Task
539 INFO: [Timing 38-35] 79-Done setting XDC timing constraints.
540 Ending Cache Timing Information Task | Checksum: 19fe8cb97
542 Time (s): cpu = 00:00:09 ; elapsed = 00:00:09 . Memory (MB): peak = 1370.594 ; gain = 493.266
544 """
545 # _PARSERS: ClassVar[Tuple[Type["SubTask"], ...]]
547 _subtasks: Dict[Type["SubTask"], "SubTask"]
549 def __init__(self, command: "Command") -> None:
550 super().__init__(command)
552 self._subtasks = {p: p(self) for p in self._PARSERS}
554 @readonly
555 def SubTasks(self) -> Dict[Type["SubTask"], "SubTask"]:
556 return self._subtasks
558 def __contains__(self, key: Any) -> bool:
559 if not issubclass(key, SubTask): 559 ↛ 560line 559 didn't jump to line 560 because the condition on line 559 was never true
560 ex = TypeError(f"Parameter 'key' is not a Subtask.")
561 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
562 raise ex
564 return key in self._subtasks
566 def __getitem__(self, key: Type["SubTask"]) -> "SubTask":
567 try:
568 return self._subtasks[key]
569 except KeyError as ex:
570 raise SubTaskNotPresentException(F"Subtask '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
572 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
573 line = yield from self._TaskStart(line)
575 activeParsers: List[Phase] = list(self._subtasks.values())
577 while True:
578 while True:
579 if line._kind is LineKind.Empty:
580 line = yield line
581 continue
582 elif isinstance(line, VivadoMessage):
583 self._AddMessage(line)
584 elif line.StartsWith("Starting "):
585 for parser in activeParsers: # type: SubTask 585 ↛ 590line 585 didn't jump to line 590 because the loop on line 585 didn't complete
586 if line.StartsWith(parser._START): 586 ↛ 585line 586 didn't jump to line 585 because the condition on line 586 was always true
587 line = yield next(subtask := parser.Generator(line))
588 break
589 else:
590 WarningCollector.Raise(UnknownSubTask(f"Unknown subtask: '{line!r}'", line))
591 ex = Exception(f"How to recover from here? Unknown subtask: '{line!r}'")
592 ex.add_note(f"Current task: start pattern='{self}'")
593 ex.add_note(f"Current command: {self._command}")
594 raise ex
595 break
596 elif line.StartsWith("Ending"):
597 nextLine = yield from self._TaskFinish(line)
598 return nextLine
599 elif line.StartsWith(self._TIME): 599 ↛ 600line 599 didn't jump to line 600 because the condition on line 599 was never true
600 line._kind = LineKind.TaskTime
601 nextLine = yield line
602 return nextLine
604 line = yield line
606 while True:
607 isFinish = False # line.StartsWith("Ending") # FIXME: detect end, but time might come later
609 try:
610 processedLine = subtask.send(line)
612 if isinstance(processedLine, VivadoMessage):
613 self._AddMessage(processedLine)
615 if isFinish: 615 ↛ 616line 615 didn't jump to line 616 because the condition on line 615 was never true
616 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
617 line = yield processedLine
618 break
619 except StopIteration as ex:
620 activeParsers.remove(parser)
621 line = ex.value
622 break
624 line = yield processedLine
627@export
628class SubTask(BaseParser, VivadoMessagesMixin):
629 # _NAME: ClassVar[str]
630 # _START: ClassVar[str]
631 # _FINISH: ClassVar[str]
632 _TIME: ClassVar[str] = "Time (s):"
634 _task: TaskWithSubTasks
635 _duration: float
637 def __init__(self, task: TaskWithSubTasks) -> None:
638 super().__init__()
639 VivadoMessagesMixin.__init__(self)
641 self._task = task
643 @readonly
644 def Task(self) -> TaskWithSubTasks:
645 return self._task
647 def _TaskStart(self, line: Line) -> Generator[Line, Line, Line]:
648 if not line.StartsWith(self._START): 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true
649 raise ProcessorException(f"{self.__class__.__name__}._TaskStart(): Expected '{self._START}' at line {line._lineNumber}.")
651 line._kind = LineKind.TaskStart
652 nextLine = yield line
653 return nextLine
655 def _TaskFinish(self, line: Line) -> Generator[Line, Line, Line]:
656 if not line.StartsWith(self._FINISH): 656 ↛ 657line 656 didn't jump to line 657 because the condition on line 656 was never true
657 raise ProcessorException(f"{self.__class__.__name__}._TaskFinish(): Expected '{self._FINISH}' at line {line._lineNumber}.")
659 line._kind = LineKind.TaskEnd
660 line = yield line
661 while self._TIME is not None: 661 ↛ 668line 661 didn't jump to line 668 because the condition on line 661 was always true
662 if line.StartsWith(self._TIME):
663 line._kind = LineKind.TaskTime
664 break
666 line = yield line
668 nextLine = yield line
669 return nextLine
671 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
672 line = yield from self._TaskStart(line)
674 while True:
675 if line._kind is LineKind.Empty: 675 ↛ 676line 675 didn't jump to line 676 because the condition on line 675 was never true
676 line = yield line
677 continue
678 elif self._FINISH is not None and line.StartsWith("Ending"):
679 break
680 elif isinstance(line, VivadoMessage):
681 self._AddMessage(line)
682 elif line.StartsWith(self._TIME): 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true
683 line._kind = LineKind.TaskTime
684 nextLine = yield line
685 return nextLine
687 line = yield line
689 nextLine = yield from self._TaskFinish(line)
690 return nextLine
692 def __str__(self) -> str:
693 return self._NAME
696@export
697class TaskWithPhases(Task):
698 # _PARSERS: ClassVar[Tuple[Type["Phase"], ...]]
700 _phases: Dict[Type["Phase"], "Phase"]
702 def __init__(self, command: "Command") -> None:
703 super().__init__(command)
705 self._phases = {p: p(self) for p in self._PARSERS}
707 @readonly
708 def Phases(self) -> Dict[Type["Phase"], "Phase"]:
709 return self._phases
711 def __contains__(self, key: Any) -> bool:
712 if not issubclass(key, Phase): 712 ↛ 713line 712 didn't jump to line 713 because the condition on line 712 was never true
713 ex = TypeError(f"Parameter 'key' is not a Phase.")
714 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
715 raise ex
717 return key in self._phases
719 def __getitem__(self, key: Type["Phase"]) -> "Phase":
720 try:
721 return self._phases[key]
722 except KeyError as ex:
723 raise PhaseNotPresentException(F"Phase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
725 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
726 line = yield from self._TaskStart(line)
728 activeParsers: List[Phase] = list(self._phases.values())
730 while True:
731 while True:
732 if line._kind is LineKind.Empty:
733 line = yield line
734 continue
735 elif isinstance(line, VivadoMessage):
736 self._AddMessage(line)
737 elif line.StartsWith("Phase "):
738 for parser in activeParsers: # type: Phase 738 ↛ 743line 738 didn't jump to line 743 because the loop on line 738 didn't complete
739 if (match := parser._START.match(line._message)) is not None:
740 line = yield next(phase := parser.Generator(line))
741 break
742 else:
743 WarningCollector.Raise(UnknownPhase(f"Unknown phase: '{line!r}'", line))
744 ex = Exception(f"How to recover from here? Unknown phase: '{line!r}'")
745 ex.add_note(f"Current task: start pattern='{self}'")
746 ex.add_note(f"Current command: {self._command}")
747 raise ex
748 break
749 elif line.StartsWith("Ending"):
750 nextLine = yield from self._TaskFinish(line)
751 return nextLine
752 elif line.StartsWith(self._TIME):
753 line._kind = LineKind.TaskTime
754 nextLine = yield line
755 return nextLine
757 line = yield line
759 while True:
760 isFinish = False #line.StartsWith("Ending")
762 try:
763 processedLine = phase.send(line)
765 if isinstance(processedLine, VivadoMessage):
766 self._AddMessage(processedLine)
768 if isFinish: 768 ↛ 769line 768 didn't jump to line 769 because the condition on line 768 was never true
769 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
770 line = yield processedLine
771 break
772 except StopIteration as ex:
773 activeParsers.remove(parser)
774 line = ex.value
775 break
777 line = yield processedLine
780@export
781class Phase(BaseParser, VivadoMessagesMixin):
782 # _NAME: ClassVar[str]
783 # _START: ClassVar[str]
784 # _FINISH: ClassVar[str]
785 _TIME: ClassVar[str] = "Time (s):"
786 _FINAL: ClassVar[Nullable[str]] = None
788 _task: TaskWithPhases
789 _phaseIndex: int
790 _duration: float
792 def __init__(self, task: TaskWithPhases) -> None:
793 super().__init__()
794 VivadoMessagesMixin.__init__(self)
796 self._task = task
797 self._phaseIndex = None
799 @readonly
800 def Task(self) -> TaskWithPhases:
801 return self._task
803 def _PhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
804 if (match := self._START.match(line._message)) is None: 804 ↛ 805line 804 didn't jump to line 805 because the condition on line 804 was never true
805 raise ProcessorException(f"{self.__class__.__name__}._PhaseStart(): Expected '{self._START}' at line {line._lineNumber}.")
807 self._phaseIndex = int(match["major"])
809 line._kind = LineKind.PhaseStart
810 nextLine = yield line
811 return nextLine
813 def _PhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
814 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
815 if not line.StartsWith(FINISH): 815 ↛ 816line 815 didn't jump to line 816 because the condition on line 815 was never true
816 raise ProcessorException(f"{self.__class__.__name__}._PhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
818 line._kind = LineKind.PhaseEnd
819 line = yield line
821 if self._TIME is not None: 821 ↛ 833line 821 didn't jump to line 833 because the condition on line 821 was always true
822 while True:
823 if line.StartsWith(self._TIME):
824 line._kind = LineKind.PhaseTime
825 break
826 elif isinstance(line, VivadoMessage): 826 ↛ 827line 826 didn't jump to line 827 because the condition on line 826 was never true
827 self._AddMessage(line)
829 line = yield line
831 line = yield line
833 if self._FINAL is not None and self._task._command._processor._preamble._toolVersion >= "2023.2":
834 while True:
835 if line.StartsWith(self._FINAL): 835 ↛ 838line 835 didn't jump to line 838 because the condition on line 835 was always true
836 line._kind = LineKind.PhaseFinal
837 break
838 elif isinstance(line, VivadoMessage):
839 self._AddMessage(line)
841 line = yield line
843 line = yield line
845 # TODO: optionally collect following INFO messages like 31-389, 31-1021, 31-662
847 return line
849 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
850 line = yield from self._PhaseStart(line)
852 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
854 while True:
855 if line._kind is LineKind.Empty:
856 line = yield line
857 continue
858 elif isinstance(line, VivadoMessage):
859 self._AddMessage(line)
860 elif line.StartsWith(FINISH):
861 break
863 line = yield line
865 nextLine = yield from self._PhaseFinish(line)
866 return nextLine
868 def __str__(self) -> str:
869 return f"{self.__class__.__name__}: {self._START.pattern}"
872@export
873class PhaseWithChildren(Phase):
874 _SUBPHASE_PREFIX: ClassVar[str] = "Phase {phaseIndex}."
876 _subPhases: Dict[Type["SubPhase"], "SubPhase"]
878 def __init__(self, task: TaskWithPhases) -> None:
879 super().__init__(task)
881 self._subPhases = {p: p(self) for p in self._PARSERS}
883 def __contains__(self, key: Any) -> bool:
884 if not issubclass(key, SubPhase):
885 ex = TypeError(f"Parameter 'item' is not a SubPhase.")
886 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
887 raise ex
889 return key in self._subPhases
891 def __getitem__(self, key: Type["SubPhase"]) -> "SubPhase":
892 try:
893 return self._subPhases[key]
894 except KeyError as ex:
895 raise PhaseNotPresentException(F"SubPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
897 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
898 line = yield from self._PhaseStart(line)
900 activeParsers: List[SubPhase] = list(self._subPhases.values())
902 SUBPHASE_PREFIX = self._SUBPHASE_PREFIX.format(phaseIndex=self._phaseIndex)
903 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
905 while True:
906 while True:
907 if line._kind is LineKind.Empty:
908 line = yield line
909 continue
910 elif isinstance(line, VivadoMessage):
911 self._AddMessage(line)
912 elif line.StartsWith(SUBPHASE_PREFIX):
913 for parser in activeParsers: # type: Section 913 ↛ 918line 913 didn't jump to line 918 because the loop on line 913 didn't complete
914 if (match := parser._START.match(line._message)) is not None:
915 line = yield next(phase := parser.Generator(line))
916 break
917 else:
918 WarningCollector.Raise(UnknownSubPhase(f"Unknown subphase: '{line!r}'", line))
919 ex = Exception(f"How to recover from here? Unknown subphase: '{line!r}'")
920 ex.add_note(f"Current phase: start pattern='{self}'")
921 ex.add_note(f"Current task: start pattern='{self._task}'")
922 ex.add_note(f"Current command: {self._task._command}")
923 raise ex
924 break
925 elif line.StartsWith(FINISH):
926 nextLine = yield from self._PhaseFinish(line)
927 return nextLine
929 line = yield line
931 while True:
932 isFinish = False # line.StartsWith(SUBPHASE_PREFIX) # FIXME: detect end, but end (e.g. time) is later then ending text
934 try:
935 processedLine = phase.send(line)
937 if isinstance(processedLine, VivadoMessage):
938 self._AddMessage(processedLine)
940 if isFinish: 940 ↛ 941line 940 didn't jump to line 941 because the condition on line 940 was never true
941 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
942 line = yield processedLine
943 break
944 except StopIteration as ex:
945 activeParsers.remove(parser)
946 line = ex.value
947 break
949 line = yield processedLine
952@export
953class SubPhase(BaseParser, VivadoMessagesMixin):
954 # _NAME: ClassVar[str]
955 # _START: ClassVar[str]
956 # _FINISH: ClassVar[str]
958 _phase: Phase
959 _phaseIndex: int
960 _subPhaseIndex: int
961 _duration: float
963 def __init__(self, phase: Phase) -> None:
964 super().__init__()
965 VivadoMessagesMixin.__init__(self)
967 self._phase = phase
968 self._phaseIndex = None
969 self._subPhaseIndex = None
971 def _SubPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
972 if (match := self._START.match(line._message)) is None: 972 ↛ 973line 972 didn't jump to line 973 because the condition on line 972 was never true
973 raise ProcessorException(f"{self.__class__.__name__}._SubPhaseStart(): Expected '{self._START}' at line {line._lineNumber}.")
975 self._phaseIndex = int(match["major"])
976 self._subPhaseIndex = int(match["minor"])
978 line._kind = LineKind.SubPhaseStart
979 nextLine = yield line
980 return nextLine
982 def _SubPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
983 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex)
985 if not line.StartsWith(FINISH): 985 ↛ 986line 985 didn't jump to line 986 because the condition on line 985 was never true
986 raise ProcessorException(f"{self.__class__.__name__}._SubPhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
988 if self._TIME is None:
989 line._kind = LineKind.SubPhaseTime
990 else:
991 line._kind = LineKind.SubPhaseEnd
993 line = yield line
994 while self._TIME is not None: 994 ↛ 1001line 994 didn't jump to line 1001 because the condition on line 994 was always true
995 if line.StartsWith(self._TIME):
996 line._kind = LineKind.SubPhaseTime
997 break
999 line = yield line
1001 nextLine = yield line
1002 return nextLine
1004 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1005 line = yield from self._SubPhaseStart(line)
1007 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex)
1009 while True:
1010 if line._kind is LineKind.Empty:
1011 line = yield line
1012 continue
1013 elif line.StartsWith(FINISH):
1014 break
1015 elif isinstance(line, VivadoMessage):
1016 self._AddMessage(line)
1018 line = yield line
1020 nextLine = yield from self._SubPhaseFinish(line)
1021 return nextLine
1023 def __str__(self) -> str:
1024 return f"{self.__class__.__name__}: {self._START.pattern}"
1027@export
1028class SubPhaseWithChildren(SubPhase):
1029 _subSubPhases: Dict[Type["SubSubPhase"], "SubSubPhase"]
1031 def __init__(self, phase: Phase) -> None:
1032 super().__init__(phase)
1034 self._subSubPhases = {p: p(self) for p in self._PARSERS}
1036 def __contains__(self, key: Any) -> bool:
1037 if not issubclass(key, SubSubPhase):
1038 ex = TypeError(f"Parameter 'item' is not a SubSubPhase.")
1039 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1040 raise ex
1042 return key in self._subSubPhases
1044 def __getitem__(self, key: Type["SubSubPhase"]) -> "SubSubPhase":
1045 try:
1046 return self._subSubPhases[key]
1047 except KeyError as ex:
1048 raise PhaseNotPresentException(F"SubSubPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
1050 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1051 line = yield from self._SubPhaseStart(line)
1053 activeParsers: List["SubSubPhase"] = list(self._subSubPhases.values())
1055 START_PREFIX = f"Phase {self._phaseIndex}.{self._subPhaseIndex}."
1056 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex)
1058 while True:
1059 while True:
1060 if line._kind is LineKind.Empty:
1061 line = yield line
1062 continue
1063 elif isinstance(line, VivadoMessage):
1064 self._AddMessage(line)
1065 elif line.StartsWith(START_PREFIX):
1066 for parser in activeParsers: # type: SubSubPhase 1066 ↛ 1071line 1066 didn't jump to line 1071 because the loop on line 1066 didn't complete
1067 if (match := parser._START.match(line._message)) is not None:
1068 line = yield next(phase := parser.Generator(line))
1069 break
1070 else:
1071 WarningCollector.Raise(UnknownSubPhase(f"Unknown subsubphase: '{line!r}'", line))
1072 ex = Exception(f"How to recover from here? Unknown subsubphase: '{line!r}'")
1073 ex.add_note(f"Current subphase: start pattern='{self}'")
1074 ex.add_note(f"Current phase: start pattern='{self._phase}'")
1075 ex.add_note(f"Current task: start pattern='{self._phase._task}'")
1076 ex.add_note(f"Current cmd: {self._phase._task._command}")
1077 raise ex
1078 break
1079 elif line.StartsWith(FINISH): 1079 ↛ 1083line 1079 didn't jump to line 1083 because the condition on line 1079 was always true
1080 nextLine = yield from self._SubPhaseFinish(line)
1081 return nextLine
1083 line = yield line
1085 while True:
1086 isFinish = False # line.StartsWith("Ending")
1088 try:
1089 processedLine = phase.send(line)
1091 if isinstance(processedLine, VivadoMessage):
1092 self._AddMessage(processedLine)
1094 if isFinish: 1094 ↛ 1095line 1094 didn't jump to line 1095 because the condition on line 1094 was never true
1095 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
1096 line = yield processedLine
1097 break
1098 except StopIteration as ex:
1099 activeParsers.remove(parser)
1100 line = ex.value
1101 break
1103 line = yield processedLine
1106@export
1107class SubSubPhase(BaseParser, VivadoMessagesMixin):
1108 # _NAME: ClassVar[str]
1109 # _START: ClassVar[str]
1110 # _FINISH: ClassVar[str]
1112 _subphase: SubPhase
1113 _phaseIndex: int
1114 _subPhaseIndex: int
1115 _subSubPhaseIndex: int
1116 _duration: float
1118 def __init__(self, subphase: SubPhase) -> None:
1119 super().__init__()
1120 VivadoMessagesMixin.__init__(self)
1122 self._subphase = subphase
1123 self._phaseIndex = None
1124 self._subPhaseIndex = None
1125 self._subSubPhaseIndex = None
1127 def _SubSubPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
1128 if (match := self._START.match(line._message)) is None: 1128 ↛ 1129line 1128 didn't jump to line 1129 because the condition on line 1128 was never true
1129 raise ProcessorException()
1131 self._phaseIndex = int(match["major"])
1132 self._subPhaseIndex = int(match["minor"])
1133 self._subSubPhaseIndex = int(match["micro"])
1135 line._kind = LineKind.SubSubPhaseStart
1136 nextLine = yield line
1137 return nextLine
1139 def _SubSubPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
1140 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex)
1142 if not line.StartsWith(FINISH): 1142 ↛ 1143line 1142 didn't jump to line 1143 because the condition on line 1142 was never true
1143 raise ProcessorException(f"{self.__class__.__name__}._SubSubPhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
1145 line._kind = LineKind.SubSubPhaseEnd
1146 line = yield line
1148 while self._TIME is not None: 1148 ↛ 1155line 1148 didn't jump to line 1155 because the condition on line 1148 was always true
1149 if line.StartsWith(self._TIME):
1150 line._kind = LineKind.SubSubPhaseTime
1151 break
1153 line = yield line
1155 nextLine = yield line
1156 return nextLine
1158 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1159 line = yield from self._SubSubPhaseStart(line)
1161 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex)
1163 while True:
1164 if line._kind is LineKind.Empty:
1165 line = yield line
1166 continue
1167 elif line.StartsWith(FINISH):
1168 break
1169 elif isinstance(line, VivadoMessage):
1170 self._AddMessage(line)
1172 line = yield line
1174 nextLine = yield from self._SubSubPhaseFinish(line)
1175 return nextLine
1177 def __str__(self) -> str:
1178 return f"{self.__class__.__name__}: {self._START.pattern}"
1181@export
1182class SubSubPhaseWithChildren(SubSubPhase):
1183 _subSubSubPhases: Dict[Type["SubSubSubPhase"], "SubSubSubPhase"]
1185 def __init__(self, subphase: SubPhase) -> None:
1186 super().__init__(subphase)
1188 self._subSubSubPhases = {p: p(self) for p in self._PARSERS}
1190 def __contains__(self, key: Any) -> bool:
1191 if not issubclass(key, SubSubSubPhase):
1192 ex = TypeError(f"Parameter 'item' is not a SubSubSubPhase.")
1193 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1194 raise ex
1196 return key in self._subSubSubPhases
1198 def __getitem__(self, key: Type["SubSubSubPhase"]) -> "SubSubSubPhase":
1199 try:
1200 return self._subSubSubPhases[key]
1201 except KeyError as ex:
1202 raise PhaseNotPresentException(F"SubSubSubPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
1204 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1205 line = yield from self._SubSubPhaseStart(line)
1207 activeParsers: List["SubSubSubPhase"] = list(self._subSubSubPhases.values())
1209 START_PREFIX = f"Phase {self._phaseIndex}.{self._subPhaseIndex}.{self._subSubPhaseIndex}."
1211 while True:
1212 while True:
1213 if line._kind is LineKind.Empty:
1214 line = yield line
1215 continue
1216 elif isinstance(line, VivadoMessage):
1217 self._AddMessage(line)
1218 elif line.StartsWith(START_PREFIX):
1219 for parser in activeParsers: # type: SubSubSubPhase 1219 ↛ 1224line 1219 didn't jump to line 1224 because the loop on line 1219 didn't complete
1220 if (match := parser._START.match(line._message)) is not None: 1220 ↛ 1219line 1220 didn't jump to line 1219 because the condition on line 1220 was always true
1221 line = yield next(phase := parser.Generator(line))
1222 break
1223 else:
1224 WarningCollector.Raise(UnknownSubPhase(f"Unknown subsubsubphase: '{line!r}'", line))
1225 ex = Exception(f"How to recover from here? Unknown subsubsubphase: '{line!r}'")
1226 ex.add_note(f"Current subsubphase: start pattern='{self}'")
1227 ex.add_note(f"Current subphase: start pattern='{self._subphase}'")
1228 ex.add_note(f"Current phase: start pattern='{self._subphase._phase}'")
1229 ex.add_note(f"Current task: start pattern='{self._subphase._phase._task}'")
1230 ex.add_note(f"Current cmd: {self._subphase._phase._task._command}")
1231 raise ex
1232 break
1233 elif line.StartsWith(self._TIME):
1234 line._kind = LineKind.SubSubPhaseTime
1235 nextLine = yield line
1236 return nextLine
1238 line = yield line
1240 while True:
1241 isFinish = False # line.StartsWith("Ending")
1243 try:
1244 processedLine = phase.send(line)
1246 if isinstance(processedLine, VivadoMessage):
1247 self._AddMessage(processedLine)
1249 if isFinish: 1249 ↛ 1250line 1249 didn't jump to line 1250 because the condition on line 1249 was never true
1250 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
1251 line = yield processedLine
1252 break
1253 except StopIteration as ex:
1254 activeParsers.remove(parser)
1255 line = ex.value
1256 break
1258 line = yield processedLine
1261@export
1262class SubSubSubPhase(BaseParser, VivadoMessagesMixin):
1263 # _NAME: ClassVar[str]
1264 # _START: ClassVar[str]
1265 # _FINISH: ClassVar[str]
1267 _subsubphase: SubSubPhase
1268 _phaseIndex: int
1269 _subPhaseIndex: int
1270 _subSubPhaseIndex: int
1271 _subSubSubPhaseIndex: int
1272 _duration: float
1274 def __init__(self, subsubphase: SubSubPhase) -> None:
1275 super().__init__()
1276 VivadoMessagesMixin.__init__(self)
1278 self._subsubphase = subsubphase
1279 self._phaseIndex = None
1280 self._subPhaseIndex = None
1281 self._subSubPhaseIndex = None
1282 self._subSubSubPhaseIndex = None
1284 def _SubSubSubPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
1285 if (match := self._START.match(line._message)) is None: 1285 ↛ 1286line 1285 didn't jump to line 1286 because the condition on line 1285 was never true
1286 raise ProcessorException()
1288 self._phaseIndex = int(match["major"])
1289 self._subPhaseIndex = int(match["minor"])
1290 self._subSubPhaseIndex = int(match["micro"])
1291 self._subSubSubPhaseIndex = int(match["nano"])
1293 line._kind = LineKind.SubSubSubPhaseStart
1294 nextLine = yield line
1295 return nextLine
1297 def _SubSubSubPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
1298 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex, subSubSubPhaseIndex=self._subSubSubPhaseIndex)
1300 if not line.StartsWith(FINISH): 1300 ↛ 1301line 1300 didn't jump to line 1301 because the condition on line 1300 was never true
1301 raise ProcessorException(f"{self.__class__.__name__}._SubSubSubPhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
1303 line._kind = LineKind.SubSubSubPhaseEnd
1304 line = yield line
1306 while self._TIME is not None: 1306 ↛ 1313line 1306 didn't jump to line 1313 because the condition on line 1306 was always true
1307 if line.StartsWith(self._TIME):
1308 line._kind = LineKind.SubSubSubPhaseTime
1309 break
1311 line = yield line
1313 nextLine = yield line
1314 return nextLine
1316 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1317 line = yield from self._SubSubSubPhaseStart(line)
1319 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex, subPhaseIndex=self._subPhaseIndex, subSubPhaseIndex=self._subSubPhaseIndex, subSubSubPhaseIndex=self._subSubSubPhaseIndex)
1321 while True:
1322 if line._kind is LineKind.Empty: 1322 ↛ 1323line 1322 didn't jump to line 1323 because the condition on line 1322 was never true
1323 line = yield line
1324 continue
1325 elif line.StartsWith(FINISH):
1326 break
1327 elif isinstance(line, VivadoMessage): 1327 ↛ 1330line 1327 didn't jump to line 1330 because the condition on line 1327 was always true
1328 self._AddMessage(line)
1330 line = yield line
1332 nextLine = yield from self._SubSubSubPhaseFinish(line)
1333 return nextLine
1335 def __str__(self) -> str:
1336 return f"{self.__class__.__name__}: {self._START.pattern}"
1339@export
1340class SubSubSubPhaseWithTasks(SubSubSubPhase):
1341 _nestedTasks: Dict[Type["NestedTask"], "NestedTask"]
1343 def __init__(self, subsubphase: SubSubPhase) -> None:
1344 super().__init__(subsubphase)
1346 self._nestedTasks = {p: p(self) for p in self._PARSERS}
1348 def __contains__(self, key: Any) -> bool:
1349 if not issubclass(key, NestedTask):
1350 ex = TypeError(f"Parameter 'key' is not a NestedTask.")
1351 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1352 raise ex
1354 return key in self._nestedTasks
1356 def __getitem__(self, key: Type["NestedTask"]) -> "NestedTask":
1357 try:
1358 return self._nestedTasks[key]
1359 except KeyError as ex:
1360 raise NestedTaskNotPresentException(F"NestedTask '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
1362 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1363 line = yield from self._SubSubSubPhaseStart(line)
1365 activeParsers: List["NestedTask"] = list(self._nestedTasks.values())
1367 # START_PREFIX = f"Phase {self._phaseIndex}.{self._subPhaseIndex}.{self._subSubPhaseIndex}."
1369 while True:
1370 while True:
1371 if line._kind is LineKind.Empty:
1372 line = yield line
1373 continue
1374 elif isinstance(line, VivadoMessage):
1375 self._AddMessage(line)
1376 elif line.StartsWith("Starting "):
1377 for parser in activeParsers: # type: NestedTask 1377 ↛ 1382line 1377 didn't jump to line 1382 because the loop on line 1377 didn't complete
1378 if line.StartsWith(parser._START): 1378 ↛ 1377line 1378 didn't jump to line 1377 because the condition on line 1378 was always true
1379 line = yield next(phase := parser.Generator(line))
1380 break
1381 else:
1382 WarningCollector.Raise(UnknownSubPhase(f"Unknown NestedTask: '{line!r}'", line))
1383 ex = Exception(f"How to recover from here? Unknown NestedTask: '{line!r}'")
1384 ex.add_note(f"Current subsubsubphase: start pattern='{self}'")
1385 ex.add_note(f"Current subsubphase: start pattern='{self._subsubphase}'")
1386 ex.add_note(f"Current subphase: start pattern='{self._subsubphase._subphase}'")
1387 ex.add_note(f"Current phase: start pattern='{self._subsubphase._subphase._phase}'")
1388 ex.add_note(f"Current task: start pattern='{self._subsubphase._subphase._phase._task}'")
1389 ex.add_note(f"Current cmd: {self._subsubphase._subphase._phase._task._command}")
1390 raise ex
1391 break
1392 elif line.StartsWith(self._TIME):
1393 line._kind = LineKind.SubSubSubPhaseTime
1394 nextLine = yield line
1395 return nextLine
1397 line = yield line
1399 while True:
1400 isFinish = False # line.StartsWith("Ending")
1402 try:
1403 processedLine = phase.send(line)
1405 if isinstance(processedLine, VivadoMessage):
1406 self._AddMessage(processedLine)
1408 if isFinish: 1408 ↛ 1409line 1408 didn't jump to line 1409 because the condition on line 1408 was never true
1409 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
1410 line = yield processedLine
1411 break
1412 except StopIteration as ex:
1413 activeParsers.remove(parser)
1414 line = ex.value
1415 break
1417 line = yield processedLine
1420@export
1421class NestedTask(BaseParser, VivadoMessagesMixin):
1422 # _NAME: ClassVar[str]
1423 # _START: ClassVar[str]
1424 # _FINISH: ClassVar[str]
1425 _TIME: ClassVar[str] = "Time (s):"
1427 _subsubsubphase: SubSubSubPhaseWithTasks
1428 _duration: float
1430 def __init__(self, subsubsubphase: SubSubSubPhaseWithTasks) -> None:
1431 super().__init__()
1432 VivadoMessagesMixin.__init__(self)
1434 self._subsubsubphase = subsubsubphase
1436 @readonly
1437 def SubSubSubPhase(self) -> SubSubSubPhaseWithTasks:
1438 return self._subsubsubphase
1440 def _NestedTaskStart(self, line: Line) -> Generator[Line, Line, Line]:
1441 if not line.StartsWith(self._START): 1441 ↛ 1442line 1441 didn't jump to line 1442 because the condition on line 1441 was never true
1442 raise ProcessorException(f"{self.__class__.__name__}._TaskStart(): Expected '{self._START}' at line {line._lineNumber}.")
1444 line._kind = LineKind.NestedTaskStart
1445 nextLine = yield line
1446 return nextLine
1448 def _NestedTaskFinish(self, line: Line) -> Generator[Line, Line, Line]:
1449 if not line.StartsWith(self._FINISH): 1449 ↛ 1450line 1449 didn't jump to line 1450 because the condition on line 1449 was never true
1450 raise ProcessorException(f"{self.__class__.__name__}._TaskFinish(): Expected '{self._FINISH}' at line {line._lineNumber}.")
1452 line._kind = LineKind.NestedTaskEnd
1453 line = yield line
1455 if self._TIME is not None: 1455 ↛ 1464line 1455 didn't jump to line 1464 because the condition on line 1455 was always true
1456 while True:
1457 if line.StartsWith(self._TIME):
1458 line._kind = LineKind.TaskTime
1459 line = yield line
1460 break
1462 line = yield line
1464 return line
1466 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1467 line = yield from self._NestedTaskStart(line)
1469 while True:
1470 if line._kind is LineKind.Empty:
1471 line = yield line
1472 continue
1473 elif self._FINISH is not None and line.StartsWith("Ending"):
1474 break
1475 elif isinstance(line, VivadoMessage):
1476 self._AddMessage(line)
1477 elif line.StartsWith(self._TIME):
1478 line._kind = LineKind.TaskTime
1479 nextLine = yield line
1480 return nextLine
1482 line = yield line
1484 nextLine = yield from self._NestedTaskFinish(line)
1485 return nextLine
1487 def __str__(self) -> str:
1488 return self._NAME
1491@export
1492class NestedTaskWithPhases(NestedTask):
1493 """
1494 A task's output emitted by a Vivado command.
1496 .. rubric:: Extracted information
1498 * Vivado messages (info, warning, critical warning, error).
1499 * Nested phases
1501 .. rubric:: Example
1503 .. code-block::
1505 Phase 4.1.1.1 BUFG Insertion
1507 Starting Physical Synthesis Task
1509 Phase 1 Physical Synthesis Initialization
1510 INFO: [Physopt 32-721] Multithreading enabled for phys_opt_design using a maximum of 2 CPUs
1511 INFO: [Physopt 32-619] Estimated Timing Summary | WNS=-0.733 | TNS=-0.936 |
1512 Phase 1 Physical Synthesis Initialization | Checksum: 1818afcc0
1514 Time (s): cpu = 00:00:00 ; elapsed = 00:00:00.014 . Memory (MB): peak = 1865.645 ; gain = 0.000
1515 INFO: [Place 46-56] BUFG insertion identified 0 candidate nets. Inserted BUFG: 0, Replicated BUFG Driver: 0, Skipped due to Placement/Routing Conflicts: 0, Skipped due to Timing Degradation: 0, Skipped due to netlist editing failed: 0.
1516 Ending Physical Synthesis Task | Checksum: 22839c186
1518 Time (s): cpu = 00:00:00 ; elapsed = 00:00:00.016 . Memory (MB): peak = 1865.645 ; gain = 0.000
1519 Phase 4.1.1.1 BUFG Insertion | Checksum: 1a8cbaaf2
1520 """
1521 # _PARSERS: ClassVar[Tuple[Type["SubTask"], ...]]
1523 _nestedPhases: Dict[Type["NestedPhase"], "NestedPhase"]
1525 def __init__(self, subsubsubPhase: SubSubSubPhaseWithTasks) -> None:
1526 super().__init__(subsubsubPhase)
1528 self._nestedPhases = {p: p(self) for p in self._PARSERS}
1530 @readonly
1531 def NestedPhases(self) -> Dict[Type["NestedPhase"], "NestedPhase"]:
1532 return self._nestedPhases
1534 def __contains__(self, key: Any) -> bool:
1535 if not issubclass(key, NestedPhase):
1536 ex = TypeError(f"Parameter 'key' is not a NestedPhase.")
1537 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
1538 raise ex
1540 return key in self._nestedPhases
1542 def __getitem__(self, key: Type["NestedPhase"]) -> "NestedPhase":
1543 try:
1544 return self._nestedPhases[key]
1545 except KeyError as ex:
1546 raise SubTaskNotPresentException(F"NestedPhase '{key._NAME}' not present in '{self._parent.logfile}'.") from ex
1548 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1549 line = yield from self._NestedTaskStart(line)
1551 activeParsers: List[Phase] = list(self._nestedPhases.values())
1553 while True:
1554 while True:
1555 if line._kind is LineKind.Empty:
1556 line = yield line
1557 continue
1558 elif isinstance(line, VivadoMessage):
1559 self._AddMessage(line)
1560 elif line.StartsWith("Phase "):
1561 for parser in activeParsers: # type: NestedPhase 1561 ↛ 1566line 1561 didn't jump to line 1566 because the loop on line 1561 didn't complete
1562 if (match := parser._START.match(line._message)) is not None: 1562 ↛ 1561line 1562 didn't jump to line 1561 because the condition on line 1562 was always true
1563 line = yield next(phase := parser.Generator(line))
1564 break
1565 else:
1566 WarningCollector.Raise(UnknownSubPhase(f"Unknown NestedPhase: '{line!r}'", line))
1567 ex = Exception(f"How to recover from here? Unknown NestedPhase: '{line!r}'")
1568 ex.add_note(f"Current nestedtask: start pattern='{self}'")
1569 ex.add_note(f"Current subsubsubphase: start pattern='{self._subsubsubphase}'")
1570 ex.add_note(f"Current subsubphase: start pattern='{self._subsubsubphase._subsubphase}'")
1571 ex.add_note(f"Current subphase: start pattern='{self._subsubsubphase._subsubphase._subphase}'")
1572 ex.add_note(f"Current phase: start pattern='{self._subsubsubphase._subsubphase._subphase._phase}'")
1573 ex.add_note(f"Current task: start pattern='{self._subsubsubphase._subsubphase._subphase._phase._task}'")
1574 ex.add_note(f"Current cmd: {self._subsubsubphase._subsubphase._subphase._phase._task._command}")
1575 raise ex
1576 break
1577 elif line.StartsWith("Ending"): 1577 ↛ 1580line 1577 didn't jump to line 1580 because the condition on line 1577 was always true
1578 nextLine = yield from self._NestedTaskFinish(line)
1579 return nextLine
1580 elif line.StartsWith(self._TIME):
1581 line._kind = LineKind.TaskTime
1582 nextLine = yield line
1583 return nextLine
1585 line = yield line
1587 while True:
1588 isFinish = False # line.StartsWith("Ending") # FIXME: detect end, but time might come later
1590 try:
1591 processedLine = phase.send(line)
1593 if isinstance(processedLine, VivadoMessage):
1594 self._AddMessage(processedLine)
1596 if isFinish: 1596 ↛ 1597line 1596 didn't jump to line 1597 because the condition on line 1596 was never true
1597 WarningCollector.Raise(UndetectedEnd(f"Didn't detect finish: '{processedLine!r}'", processedLine))
1598 line = yield processedLine
1599 break
1600 except StopIteration as ex:
1601 activeParsers.remove(parser)
1602 line = ex.value
1603 break
1605 line = yield processedLine
1608@export
1609class NestedPhase(BaseParser, VivadoMessagesMixin):
1610 # _NAME: ClassVar[str]
1611 # _START: ClassVar[str]
1612 # _FINISH: ClassVar[str]
1613 _TIME: ClassVar[str] = "Time (s):"
1614 _FINAL: ClassVar[Nullable[str]] = None
1616 _nestedTask: NestedTaskWithPhases
1617 _phaseIndex: int
1618 _duration: float
1620 def __init__(self, nestedTask: TaskWithPhases) -> None:
1621 super().__init__()
1622 VivadoMessagesMixin.__init__(self)
1624 self._nestedTask = nestedTask
1625 self._phaseIndex = None
1627 @readonly
1628 def NestedTask(self) -> NestedTaskWithPhases:
1629 return self._nestedTask
1631 def _NestedPhaseStart(self, line: Line) -> Generator[Line, Line, Line]:
1632 if (match := self._START.match(line._message)) is None: 1632 ↛ 1633line 1632 didn't jump to line 1633 because the condition on line 1632 was never true
1633 raise ProcessorException(f"{self.__class__.__name__}._PhaseStart(): Expected '{self._START}' at line {line._lineNumber}.")
1635 self._phaseIndex = int(match["major"])
1637 line._kind = LineKind.NestedPhaseStart
1638 nextLine = yield line
1639 return nextLine
1641 def _NestedPhaseFinish(self, line: Line) -> Generator[Line, Line, None]:
1642 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
1643 if not line.StartsWith(FINISH): 1643 ↛ 1644line 1643 didn't jump to line 1644 because the condition on line 1643 was never true
1644 raise ProcessorException(f"{self.__class__.__name__}._PhaseFinish(): Expected '{FINISH}' at line {line._lineNumber}.")
1646 line._kind = LineKind.NestedPhaseEnd
1647 line = yield line
1649 if self._TIME is not None: 1649 ↛ 1661line 1649 didn't jump to line 1661 because the condition on line 1649 was always true
1650 while True:
1651 if line.StartsWith(self._TIME):
1652 line._kind = LineKind.PhaseTime
1653 break
1654 elif isinstance(line, VivadoMessage): 1654 ↛ 1655line 1654 didn't jump to line 1655 because the condition on line 1654 was never true
1655 self._AddMessage(line)
1657 line = yield line
1659 line = yield line
1661 return line
1663 def Generator(self, line: Line) -> Generator[Line, Line, Line]:
1664 line = yield from self._NestedPhaseStart(line)
1666 FINISH = self._FINISH.format(phaseIndex=self._phaseIndex)
1668 while True:
1669 if line._kind is LineKind.Empty: 1669 ↛ 1670line 1669 didn't jump to line 1670 because the condition on line 1669 was never true
1670 line = yield line
1671 continue
1672 elif isinstance(line, VivadoMessage):
1673 self._AddMessage(line)
1674 elif line.StartsWith(FINISH): 1674 ↛ 1677line 1674 didn't jump to line 1677 because the condition on line 1674 was always true
1675 break
1677 line = yield line
1679 nextLine = yield from self._NestedPhaseFinish(line)
1680 return nextLine
1682 def __str__(self) -> str:
1683 return f"{self.__class__.__name__}: {self._START.pattern}"