Coverage for pyEDAA/OutputFilter/Xilinx/__init__.py: 92%

205 statements  

« 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 pathlib import Path 

34from typing import Optional as Nullable, Dict, List, Generator, Union, Type 

35 

36from pyTooling.Decorators import export, readonly 

37from pyTooling.MetaClasses import ExtendedType 

38from pyTooling.Common import getFullyQualifiedName 

39from pyTooling.Stopwatch import Stopwatch 

40 

41from pyEDAA.OutputFilter.Xilinx.Common import LineKind, Line, VivadoStuntedInfoMessage 

42from pyEDAA.OutputFilter.Xilinx.Common import VivadoMessage, TclCommand, VivadoTclCommand 

43from pyEDAA.OutputFilter.Xilinx.Common import InfoMessage, VivadoInfoMessage, VivadoDRCInfoMessage, VivadoIrregularInfoMessage, VivadoStuntedInfoMessage 

44from pyEDAA.OutputFilter.Xilinx.Common import WarningMessage, VivadoWarningMessage, VivadoDRCWarningMessage, VivadoXPMWarningMessage, VivadoStuntedWarningMessage 

45from pyEDAA.OutputFilter.Xilinx.Common import CriticalWarningMessage, VivadoCriticalWarningMessage 

46from pyEDAA.OutputFilter.Xilinx.Common import ErrorMessage, VivadoErrorMessage 

47from pyEDAA.OutputFilter.Xilinx.Common import VHDLReportMessage 

48from pyEDAA.OutputFilter.Xilinx.Commands import Command, SynthesizeDesign, LinkDesign, OptimizeDesign, PlaceDesign, CommandNotPresentException 

49from pyEDAA.OutputFilter.Xilinx.Commands import PhysicalOptimizeDesign, RouteDesign, WriteBitstream 

50from pyEDAA.OutputFilter.Xilinx.Commands import ReportDRC, ReportMethodology, ReportPower 

51from pyEDAA.OutputFilter.Xilinx.Common2 import Preamble, VivadoMessagesMixin, Postamble 

52from pyEDAA.OutputFilter.Xilinx.Exception import ProcessorException, ClassificationException 

53 

54 

55@export 

56class Processor(VivadoMessagesMixin, metaclass=ExtendedType, slots=True): 

57 """ 

58 A processor for Vivado log outputs. 

59 

60 Each output line from Vivado gets processed and converted into a :class:`ProcessedLine` objects. Such lines form a 

61 doubly-linked list. 

62 """ 

63 _duration: float #: Duration of the observed process (e.g. start to end of synthesis). 

64 _processingDuration: float #: Duration for the log output processor to parse all log messages. 

65 

66 _lines: List[Line] #: A list of processed log message lines. 

67 _preamble: Preamble #: Reference to the Vivado preamble written after tool startup. 

68 _postamble: Postamble #: Reference to the Vivado postamble written after tool startup. 

69 _commands: Dict[Type[Command], Command] #: A dictionary of processed Vivado commands. 

70 

71 def __init__(self) -> None: 

72 """ 

73 Initializes a Vivado log output processor. 

74 """ 

75 super().__init__() 

76 

77 self._duration = 0.0 

78 self._processingDuration = 0.0 

79 

80 self._lines = [] 

81 self._preamble = None 

82 self._postamble = None 

83 self._commands = {} 

84 

85 @readonly 

86 def Lines(self) -> List[Line]: 

87 """ 

88 Read-only property to access the list of processed and classified log lines (messages). 

89 

90 :returns: A list of processed lines. 

91 """ 

92 return self._lines 

93 

94 @readonly 

95 def Preamble(self) -> Preamble: 

96 """ 

97 Read-only property to access the parsed preamble information. 

98 

99 :returns: The log output preamble. 

100 """ 

101 return self._preamble 

102 

103 @readonly 

104 def Postamble(self) -> Postamble: 

105 """ 

106 Read-only property to access the parsed postamble information. 

107 

108 :returns: The log output postamble. 

109 """ 

110 return self._postamble 

111 

112 @readonly 

113 def Commands(self) -> Dict[Type[Command], Command]: 

114 """ 

115 Read-only property to access the dictionary of processed Vivado commands. 

116 

117 :returns: The dictionary of processed Vivado commands. 

118 """ 

119 return self._commands 

120 

121 @readonly 

122 def StartDateTime(self) -> datetime: 

123 return self._preamble.StartDatetime 

124 

125 @readonly 

126 def ExitDateTime(self) -> datetime: 

127 return self._postamble.ExitDatetime 

128 

129 @readonly 

130 def Duration(self) -> float: 

131 """ 

132 Duration of the observed process (e.g. start to end of synthesis). 

133 

134 :returns: The observed process' execution duration in seconds. 

135 """ 

136 startTime = self._preamble.StartDatetime 

137 exitTime = self._postamble.ExitDatetime 

138 

139 return (exitTime - startTime).total_seconds() 

140 

141 @readonly 

142 def ProcessingDuration(self) -> float: 

143 """ 

144 Processing duration for the log output processor to parse all log messages. 

145 

146 :returns: The processing duration in seconds. 

147 """ 

148 return self._processingDuration 

149 

150 def __contains__(self, key: Type[Command]) -> bool: 

151 """ 

152 Returns True, if log outputs where found for the given command. 

153 

154 :param key: Vivado command (class). 

155 :returns: True, if the Vivado command's outputs were found in log outputs. 

156 """ 

157 if not issubclass(key, Command): 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true

158 ex = TypeError(f"Parameter 'key' is not a Command.") 

159 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.") 

160 raise ex 

161 

162 return key in self._commands 

163 

164 def __getitem__(self, key: Type[Command]) -> Command: 

165 """ 

166 Access Vivado command specific log outputs and parsed data by the command. 

167 

168 :param key: Vivado command (class) to access. 

169 :returns: A Vivado command instance with parsed log messages and extracted data. 

170 """ 

171 try: 

172 return self._commands[key] 

173 except KeyError as ex: 

174 raise CommandNotPresentException(F"Command '{key._TCL_COMMAND}' not present in '{self._logfile}'.") from ex 

175 

176 @readonly 

177 def IsIncompleteLog(self) -> bool: 

178 """ 

179 Read-only property returning true if the processed Vivado log output is incomplete. 

180 

181 A log can be incomplete, because: 

182 

183 * Vivado disabled messages, because too many messages of the same kind appeared. Usually, a message type is disabled 

184 after 100 messages of that type. This is indicated by message ``[Common 17-14]``. 

185 

186 :returns: True, if messages where silenced by Vivado. 

187 

188 .. note:: 

189 

190 .. code-block:: 

191 

192 INFO: [Common 17-14] Message 'Synth 8-3321' appears 100 times and further instances of the messages will be 

193 disabled. Use the Tcl command set_msg_config to change the current settings. 

194 """ 

195 return 17 in self._messagesByID and 14 in self._messagesByID[17] 

196 

197 def LineClassification(self) -> Generator[Line, str, None]: 

198 # Instantiate and initialize CommandFinder 

199 next(cmdFinder := self.CommandFinder()) 

200 

201 # wait for first line 

202 lastLine = None 

203 rawMessageLine = yield 

204 lineNumber = 0 

205 _errorMessage = "Unknown processing error" 

206 

207 while rawMessageLine is not None: 207 ↛ exitline 207 didn't return from function 'LineClassification' because the condition on line 207 was always true

208 lineNumber += 1 

209 rawMessageLine = rawMessageLine.rstrip() 

210 errorMessage = _errorMessage 

211 

212 if len(rawMessageLine) == 0: 

213 line = Line(lineNumber, LineKind.Empty, rawMessageLine, previousLine=lastLine) 

214 elif rawMessageLine.startswith("INFO"): 

215 if (line := VivadoInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 

216 if (line := VivadoDRCInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 

217 if (line := VivadoIrregularInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 

218 line = VivadoStuntedInfoMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine) 

219 

220 errorMessage = f"Line starting with 'INFO' was not a VivadoInfoMessage." 

221 elif rawMessageLine.startswith("WARNING"): 

222 if (line := VivadoWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 

223 if (line := VivadoDRCWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 223 ↛ 227line 223 didn't jump to line 227 because the condition on line 223 was always true

224 if (line := VivadoXPMWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine)) is None: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 line = VivadoStuntedWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine) 

226 

227 errorMessage = f"Line starting with 'WARNING' was not a VivadoWarningMessage." 

228 elif rawMessageLine.startswith("CRITICAL WARNING"): 

229 line = VivadoCriticalWarningMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine) 

230 

231 errorMessage = f"Line starting with 'CRITICAL WARNING' was not a VivadoCriticalWarningMessage." 

232 elif rawMessageLine.startswith("ERROR"): 

233 line = VivadoErrorMessage.Parse(lineNumber, rawMessageLine, previousLine=lastLine) 

234 

235 errorMessage = f"Line starting with 'ERROR' was not a VivadoErrorMessage." 

236 elif rawMessageLine.startswith("Command: "): 

237 line = VivadoTclCommand.Parse(lineNumber, rawMessageLine, previousLine=lastLine) 

238 

239 errorMessage = "Line starting with 'Command:' was not a VivadoTclCommand." 

240 else: 

241 line = Line(lineNumber, LineKind.Unprocessed, rawMessageLine, previousLine=lastLine) 

242 

243 if line.StartsWith("Resolution:") and isinstance(lastLine, VivadoMessage): 

244 line._kind = LineKind.Verbose 

245 

246 if line is None: 246 ↛ 248line 246 didn't jump to line 248 because the condition on line 246 was never true

247 # TODO: what to do with this line? attache to exception? 

248 line = Line(lineNumber, LineKind.ProcessorError, rawMessageLine, previousLine=lastLine) 

249 

250 raise ClassificationException(errorMessage, lineNumber, rawMessageLine) 

251 

252 if isinstance(line, VivadoMessage): 

253 self._AddMessage(line) 

254 

255 line = cmdFinder.send(line) 

256 

257 if line._kind is LineKind.ProcessorError: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true

258 line = ClassificationException(errorMessage, lineNumber, rawMessageLine) 

259 

260 self._lines.append(line) 

261 

262 lastLine = line 

263 rawMessageLine = yield line 

264 

265 def CommandFinder(self) -> Generator[Line, Line, None]: 

266 self._preamble = Preamble(self) 

267 self._postamble = Postamble(self) 

268 

269 tclProcedures = {"source"} 

270 

271 # wait for first line 

272 line = yield 

273 # process preamble 

274 line = yield from self._preamble.Generator(line) 

275 

276 while True: 

277 while True: 

278 if line._kind is LineKind.Empty: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true

279 line = yield line 

280 continue 

281 elif isinstance(line, VivadoInfoMessage): 

282 if line.ToolID == 17 and line.MessageKindID == 206: 

283 lastLine = yield from self._postamble.Generator(line) 

284 return lastLine 

285 elif isinstance(line, VivadoTclCommand): 

286 if line._command == SynthesizeDesign._TCL_COMMAND: 

287 self._commands[SynthesizeDesign] = (cmd := SynthesizeDesign(self)) 

288 line = yield next(gen := cmd.SectionDetector(line)) 

289 break 

290 elif line._command == LinkDesign._TCL_COMMAND: 

291 self._commands[LinkDesign] = (cmd := LinkDesign(self)) 

292 line = yield next(gen := cmd.SectionDetector(line)) 

293 break 

294 elif line._command == OptimizeDesign._TCL_COMMAND: 

295 self._commands[OptimizeDesign] = (cmd := OptimizeDesign(self)) 

296 line = yield next(gen := cmd.SectionDetector(line)) 

297 break 

298 elif line._command == PlaceDesign._TCL_COMMAND: 

299 self._commands[PlaceDesign] = (cmd := PlaceDesign(self)) 

300 line = yield next(gen := cmd.SectionDetector(line)) 

301 break 

302 elif line._command == PhysicalOptimizeDesign._TCL_COMMAND: 

303 self._commands[PhysicalOptimizeDesign] = (cmd := PhysicalOptimizeDesign(self)) 

304 line = yield next(gen := cmd.SectionDetector(line)) 

305 break 

306 elif line._command == RouteDesign._TCL_COMMAND: 

307 self._commands[RouteDesign] = (cmd := RouteDesign(self)) 

308 line = yield next(gen := cmd.SectionDetector(line)) 

309 break 

310 elif line._command == WriteBitstream._TCL_COMMAND: 

311 self._commands[WriteBitstream] = (cmd := WriteBitstream(self)) 

312 line = yield next(gen := cmd.SectionDetector(line)) 

313 break 

314 elif line._command == ReportDRC._TCL_COMMAND: 

315 self._commands[ReportDRC] = (cmd := ReportDRC(self)) 

316 line = yield next(gen := cmd.SectionDetector(line)) 

317 break 

318 elif line._command == ReportMethodology._TCL_COMMAND: 

319 self._commands[ReportMethodology] = (cmd := ReportMethodology(self)) 

320 line = yield next(gen := cmd.SectionDetector(line)) 

321 break 

322 elif line._command == ReportPower._TCL_COMMAND: 

323 self._commands[ReportPower] = (cmd := ReportPower(self)) 

324 line = yield next(gen := cmd.SectionDetector(line)) 

325 break 

326 

327 firstWord = line.Partition(" ")[0] 

328 if firstWord in tclProcedures: 

329 line = TclCommand.FromLine(line) 

330 

331 line = yield line 

332 

333 # end = f"{cmd._TCL_COMMAND} completed successfully" 

334 

335 while True: 

336 # if line.StartsWith(end): 

337 # # line._kind |= LineKind.Success 

338 # lastLine = gen.send(line) 

339 # if LineKind.Last in line._kind: 

340 # line._kind ^= LineKind.Last 

341 # line = yield lastLine 

342 # break 

343 

344 try: 

345 line = yield gen.send(line) 

346 except StopIteration as ex: 

347 line = ex.value 

348 break 

349 

350 

351@export 

352class Document(Processor): 

353 """ 

354 A Vivado log output processor for a log file. 

355 

356 This processor represents a Vivado log file (e.g. ``*.vds`` or ``*.vdi``). It processees its content line-by-line 

357 while classifying each line as a message. The processing duration is available via :data:`ProcessingDuration`. 

358 """ 

359 _logfile: Path #: Path to the processed logfile. 

360 

361 # FIXME: parse=True parameter 

362 def __init__(self, logfile: Path) -> None: 

363 """ 

364 Initializes a log file. 

365 

366 :param logfile: Path to the log file. 

367 """ 

368 super().__init__() 

369 

370 # FIXME: check if path 

371 self._logfile = logfile 

372 

373 @readonly 

374 def Logfile(self) -> Path: 

375 """ 

376 Read-only property to access the document's path. 

377 

378 :returns: Path to the log file. 

379 """ 

380 return self._logfile 

381 

382 def Parse(self) -> None: 

383 with Stopwatch() as sw: 

384 with self._logfile.open("r", encoding="utf-8") as f: 

385 content = f.read() 

386 

387 next(generator := self.LineClassification()) 

388 for rawLine in content.splitlines(): 

389 generator.send(rawLine) 

390 

391 self._processingDuration = sw.Duration