Coverage for pyEDAA/Reports/Unittesting/JUnit/NextestJUnit.py: 93%

194 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-14 02:26 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ____ _ # 

3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # 

7# |_| |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2026-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""" 

32Reader and writer for the JUnit XML dialect of cargo-nextest. 

33 

34.. seealso:: 

35 

36 :ref:`UNITTEST/SpecificDataModel/JUnit/Dialect/nextest` 

37 |rarr| The dialect's mapping to the unified data model and its known issues. 

38""" 

39from __future__ import annotations 

40 

41from datetime import datetime, timedelta 

42from pathlib import Path 

43from typing import Optional as Nullable, Iterable, Type, ClassVar 

44from uuid import UUID 

45 

46from lxml.etree import ElementTree, Element, SubElement, tostring, _Element 

47from pyTooling.Common import getFullyQualifiedName 

48from pyTooling.Decorators import export, readonly, InheritDocString, DocStringMergeStrategy 

49from pyTooling.Stopwatch import Stopwatch 

50 

51from pyEDAA.Reports.Unittesting import UnittestError, TestsuiteKind, TestcaseStatus, TestsuiteStatus, \ 

52 TestsuiteSummary as ut_TestsuiteSummary, Testsuite as ut_Testsuite, \ 

53 Testcase as ut_Testcase 

54from pyEDAA.Reports.Unittesting.JUnit import Testcase as ju_Testcase, Testclass as ju_Testclass, \ 

55 Testsuite as ju_Testsuite, TestsuiteSummary as ju_TestsuiteSummary, \ 

56 Document as ju_Document, JUnitReaderMode 

57 

58 

59@export 

60@InheritDocString(ju_Testcase, DocStringMergeStrategy.BaseLast) 

61class Testcase(ju_Testcase): 

62 """ 

63 This is a derived implementation for the cargo-nextest JUnit dialect. 

64 

65 Besides the fields of a JUnit test case, it carries the time the test case was started and the number of reruns 

66 nextest recorded for it. 

67 """ 

68 

69 _startTime: Nullable[datetime] #: Time when the test case was started. 

70 _rerunCount: int #: Number of reruns recorded as ``<flakyFailure>``, ``<rerunFailure>``, etc. 

71 

72 def __init__( 

73 self, 

74 name: str, 

75 duration: Nullable[timedelta] = None, 

76 status: TestcaseStatus = TestcaseStatus.Unknown, 

77 assertionCount: Nullable[int] = None, 

78 message: Nullable[str] = None, 

79 details: Nullable[str] = None, 

80 standardOutput: Nullable[str] = None, 

81 standardError: Nullable[str] = None, 

82 startTime: Nullable[datetime] = None, 

83 rerunCount: int = 0, 

84 *, 

85 parent: Nullable[Testclass | Testsuite] = None 

86 ) -> None: 

87 """ 

88 Initializes the fields of a test case. 

89 

90 :param name: Name of the test entity. 

91 :param duration: Optional, duration of the entity's execution. 

92 :param status: Optional, status of the test case. 

93 :param assertionCount: Optional, number of assertions within the test. 

94 :param message: Optional, message explaining the test case's status. 

95 :param details: Optional, details explaining the test case's status (e.g. a traceback). 

96 :param standardOutput: Optional, captured standard output of the test case. 

97 :param standardError: Optional, captured standard error of the test case. 

98 :param startTime: Optional, time when the test case was started. 

99 :param rerunCount: Optional, number of reruns recorded for the test case. 

100 :param parent: Optional, reference to the parent test class or test suite. 

101 :raises TypeError: If parameter 'startTime' is not a datetime. 

102 :raises ValueError: If parameter 'rerunCount' is None. 

103 :raises TypeError: If parameter 'rerunCount' is not an integer. 

104 :raises ValueError: If parameter 'rerunCount' is negative. 

105 """ 

106 super().__init__( 

107 name, duration, status, assertionCount, message, details, standardOutput, standardError, parent=parent 

108 ) 

109 

110 if startTime is not None and not isinstance(startTime, datetime): 

111 ex = TypeError("Parameter 'startTime' is not of type 'datetime'.") 

112 ex.add_note(f"Got type '{getFullyQualifiedName(startTime)}'.") 

113 raise ex 

114 

115 if rerunCount is None: 

116 raise ValueError("Parameter 'rerunCount' is None.") 

117 elif not isinstance(rerunCount, int): 

118 ex = TypeError("Parameter 'rerunCount' is not of type 'int'.") 

119 ex.add_note(f"Got type '{getFullyQualifiedName(rerunCount)}'.") 

120 raise ex 

121 elif rerunCount < 0: 

122 ex = ValueError("Parameter 'rerunCount' is negative.") 

123 ex.add_note(f"Got value '{rerunCount}'.") 

124 raise ex 

125 

126 self._startTime = startTime 

127 self._rerunCount = rerunCount 

128 

129 @readonly 

130 def StartTime(self) -> Nullable[datetime]: 

131 """ 

132 Read-only property to access the time when the test case was started (:attr:`_startTime`). 

133 

134 nextest writes no start time for a skipped test case. 

135 

136 :returns: Start time of the test case, or ``None`` if it wasn't recorded. 

137 """ 

138 return self._startTime 

139 

140 @readonly 

141 def RerunCount(self) -> int: 

142 """ 

143 Read-only property to access the number of reruns nextest recorded for the test case (:attr:`_rerunCount`). 

144 

145 A test case passing after a rerun is *flaky*: its status is passed and its rerun count is not zero. 

146 

147 :returns: Number of reruns. 

148 """ 

149 return self._rerunCount 

150 

151 def Copy(self) -> Testcase: 

152 """ 

153 Copy the test case without its parent. 

154 

155 :returns: A copy of the test case, including its start time and rerun count. 

156 """ 

157 testcase = super().Copy() 

158 testcase._startTime = self._startTime 

159 testcase._rerunCount = self._rerunCount 

160 

161 return testcase 

162 

163 @classmethod 

164 def FromTestcase(cls, testcase: ut_Testcase) -> Testcase: 

165 """ 

166 Convert a test case of the unified test entity data model to the JUnit specific data model's test case object 

167 adhering to the cargo-nextest JUnit dialect. 

168 

169 :param testcase: Test case from unified data model. 

170 :returns: Test case from JUnit specific data model (cargo-nextest JUnit dialect). 

171 """ 

172 juTestcase = super().FromTestcase(testcase) 

173 juTestcase._startTime = testcase._startTime 

174 

175 return juTestcase 

176 

177 def ToTestcase(self) -> ut_Testcase: 

178 """ 

179 Convert this test case to a test case of the unified test entity data model. 

180 

181 The start time is kept, the rerun count is lost. 

182 

183 :returns: Test case of the unified test entity data model. 

184 """ 

185 testcase = super().ToTestcase() 

186 testcase._startTime = self._startTime 

187 

188 return testcase 

189 

190 

191@export 

192@InheritDocString(ju_Testclass, DocStringMergeStrategy.BaseLast) 

193class Testclass(ju_Testclass): 

194 """ 

195 This is a derived implementation for the cargo-nextest JUnit dialect. 

196 """ 

197 

198 

199@export 

200@InheritDocString(ju_Testsuite, DocStringMergeStrategy.BaseLast) 

201class Testsuite(ju_Testsuite): 

202 """ 

203 This is a derived implementation for the cargo-nextest JUnit dialect. 

204 """ 

205 

206 @classmethod 

207 def FromTestsuite(cls, testsuite: ut_Testsuite) -> Testsuite: 

208 """ 

209 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object 

210 adhering to the cargo-nextest JUnit dialect. 

211 

212 :param testsuite: Test suite from unified data model. 

213 :returns: Test suite from JUnit specific data model (cargo-nextest JUnit dialect). 

214 :raises UnittestError: If a test case of the test suite is not part of a hierarchy. 

215 """ 

216 juTestsuite = cls( 

217 testsuite._name, 

218 hostname=testsuite._hostname, 

219 startTime=testsuite._startTime, 

220 duration=testsuite._totalDuration, 

221 status= testsuite._status, 

222 ) 

223 

224 juTestsuite._tests = testsuite._tests 

225 juTestsuite._skipped = testsuite._skipped 

226 juTestsuite._errored = testsuite._errored 

227 juTestsuite._failed = testsuite._failed 

228 juTestsuite._passed = testsuite._passed 

229 

230 for tc in testsuite.IterateTestcases(): 

231 ts = tc._parent 

232 if ts is None: 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true

233 raise UnittestError(f"Testcase '{tc._name}' is not part of a hierarchy.") 

234 elif ts is testsuite: 

235 juTestsuite.AddTestcase(Testcase.FromTestcase(tc)) 

236 continue 

237 

238 classname = ts._name 

239 ts = ts._parent 

240 while ts is not None and ts._kind > TestsuiteKind.Logical: 

241 classname = f"{ts._name}.{classname}" 

242 ts = ts._parent 

243 

244 if classname in juTestsuite._testclasses: 

245 juClass = juTestsuite._testclasses[classname] 

246 else: 

247 juClass = Testclass(classname, parent=juTestsuite) 

248 

249 juClass.AddTestcase(Testcase.FromTestcase(tc)) 

250 

251 return juTestsuite 

252 

253 

254@export 

255@InheritDocString(ju_TestsuiteSummary, DocStringMergeStrategy.BaseLast) 

256class TestsuiteSummary(ju_TestsuiteSummary): 

257 """ 

258 This is a derived implementation for the cargo-nextest JUnit dialect. 

259 """ 

260 

261 @classmethod 

262 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> TestsuiteSummary: 

263 """ 

264 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite 

265 summary object adhering to the cargo-nextest JUnit dialect. 

266 

267 :param testsuiteSummary: Test suite summary from unified data model. 

268 :returns: Test suite summary from JUnit specific data model (cargo-nextest JUnit dialect). 

269 """ 

270 return cls( 

271 testsuiteSummary._name, 

272 startTime=testsuiteSummary._startTime, 

273 duration=testsuiteSummary._totalDuration, 

274 status=testsuiteSummary._status, 

275 testsuites=(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values()) 

276 ) 

277 

278 

279@export 

280class Document(ju_Document): 

281 """ 

282 A document reader and writer for the cargo-nextest JUnit XML file format. 

283 

284 This class reads, validates and transforms an XML file in the cargo-nextest JUnit format into a JUnit data model. It 

285 can then be converted into a unified test entity data model. 

286 

287 In reverse, a JUnit data model instance with the specific cargo-nextest JUnit file format can be created from a 

288 unified test entity data model. This data model can be written as XML into a file. 

289 """ 

290 

291 _DIALECT: ClassVar[str] = "cargo-nextest + JUnit" #: Name of the dialect in messages. 

292 _TESTCASE: ClassVar[Type[Testcase]] = Testcase #: Class of the test cases read. 

293 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass #: Class of the test classes read. 

294 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite #: Class of the test suites read. 

295 

296 _runID: Nullable[UUID] #: Run ID of the test run, which wrote the report. 

297 

298 def __init__( 

299 self, 

300 xmlReportFile: Path, 

301 analyzeAndConvert: bool = False, 

302 readerMode: JUnitReaderMode = JUnitReaderMode.Default, 

303 *, 

304 name: str = "Unprocessed JUnit XML file", 

305 startTime: Nullable[datetime] = None, 

306 duration: Nullable[timedelta] = None, 

307 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

308 testsuites: Nullable[Iterable[Testsuite]] = None, 

309 runID: Nullable[UUID] = None 

310 ) -> None: 

311 """ 

312 Initializes a cargo-nextest JUnit document. 

313 

314 :param xmlReportFile: Path to the XML file. 

315 :param analyzeAndConvert: Optional, if true, read, validate and convert the file. 

316 :param readerMode: Optional, mode of the JUnit reader. 

317 :param name: Optional, name of the test suite summary. 

318 :param startTime: Optional, time when the test run was started. 

319 :param duration: Optional, duration of the test run. 

320 :param status: Optional, overall status of the test run. 

321 :param testsuites: Optional, test suites of the summary. 

322 :param runID: Optional, run ID of the test run, which wrote the report. 

323 """ 

324 super().__init__( 

325 xmlReportFile, 

326 False, 

327 readerMode, 

328 name=name, 

329 startTime=startTime, 

330 duration=duration, 

331 status=status, 

332 testsuites=testsuites 

333 ) 

334 

335 self._runID = runID 

336 

337 if analyzeAndConvert: 

338 self.Analyze() 

339 self.Convert() 

340 

341 @readonly 

342 def RunID(self) -> Nullable[UUID]: 

343 """ 

344 Read-only property to access the run ID of the test run, which wrote the report (:attr:`_runID`). 

345 

346 :returns: The run ID, or ``None`` if the report has none. 

347 """ 

348 return self._runID 

349 

350 def Analyze(self) -> None: 

351 """ 

352 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML 

353 schema. 

354 

355 .. hint:: 

356 

357 The time spend for analysis will be made available via property :data:`AnalysisDuration`. 

358 

359 The used XML schema definition is specific to the cargo-nextest JUnit dialect. 

360 """ 

361 xmlSchemaFile = "Nextest-JUnit.xsd" 

362 self._Analyze(xmlSchemaFile) 

363 

364 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None: 

365 """ 

366 Write the data model as XML into a file adhering to the cargo-nextest dialect. 

367 

368 :param path: Optional, path to the XML file, if internal path shouldn't be used. 

369 :param overwrite: Optional, if true, overwrite an existing file. 

370 :param regenerate: Optional, if true, regenerate the XML structure from data model. 

371 :raises UnittestError: If the file cannot be overwritten. 

372 :raises UnittestError: If the internal XML data structure wasn't generated. |br| 

373 Call ``Document.Generate()`` or ``Document.Write(..., regenerate=True)``. 

374 :raises UnittestError: If the file cannot be opened or written. 

375 """ 

376 if path is None: 

377 path = self._path 

378 

379 if not overwrite and path.exists(): 

380 raise UnittestError(f"JUnit XML file '{path}' can not be overwritten.") \ 

381 from FileExistsError(f"File '{path}' already exists.") 

382 

383 if regenerate: 383 ↛ 386line 383 didn't jump to line 386 because the condition on line 383 was always true

384 self.Generate(overwrite=True) 

385 

386 if self._xmlDocument is None: 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true

387 ex = UnittestError("Internal XML document tree is empty and needs to be generated before write is possible.") 

388 ex.add_note("Call 'Document.Generate()' or 'Document.Write(..., regenerate=True)'.") 

389 raise ex 

390 

391 content = tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True) 

392 try: 

393 with path.open("wb") as file: 

394 file.write(content) 

395 except OSError as ex: 

396 raise UnittestError(f"JUnit XML file '{path}' can not be written.") from ex 

397 

398 def Convert(self) -> None: 

399 """ 

400 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. 

401 

402 This method converts the root element. 

403 

404 .. hint:: 

405 

406 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. 

407 

408 :raises UnittestError: If XML was not read and parsed before. |br| 

409 Call ``Document.Analyze()`` or create the document using 

410 ``Document(path, analyzeAndConvert=True)``. 

411 """ 

412 if self._xmlDocument is None: 412 ↛ 413line 412 didn't jump to line 413 because the condition on line 412 was never true

413 ex = UnittestError(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") 

414 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.") 

415 raise ex 

416 

417 with Stopwatch() as sw: 

418 rootElement: _Element = self._xmlDocument.getroot() 

419 

420 self._name = self._ConvertName(rootElement, optional=False) 

421 self._runID = UUID(rootElement.attrib["uuid"]) if "uuid" in rootElement.attrib else None 

422 self._startTime = self._ConvertTimestamp(rootElement, optional=False) 

423 self._duration = self._ConvertTime(rootElement, optional=False) 

424 

425 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element 

426 self._ConvertTestsuite(self, rootNode) 

427 

428 self.Aggregate() 

429 

430 self._modelConversion = sw.Duration 

431 

432 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None: 

433 """ 

434 Convert the XML data structure of a ``<testsuite>`` to a test suite. 

435 

436 A ``<testsuite>`` written by nextest has neither a hostname, nor a start time, nor a duration. 

437 

438 :param parent: The test suite summary as a parent element in the test entity hierarchy. 

439 :param testsuitesNode: The current XML element node representing a test suite. 

440 """ 

441 newTestsuite = self._TESTSUITE(self._ConvertName(testsuitesNode, optional=False), parent=parent) 

442 

443 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

444 

445 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None: 

446 """ 

447 Convert the XML data structure of a ``<testcase>`` to a test case. 

448 

449 Besides the fields of a JUnit test case, the start time (``timestamp``) and the number of reruns 

450 (``<flakyFailure>``, ``<flakyError>``, ``<rerunFailure>`` and ``<rerunError>`` elements) are converted. 

451 

452 :param parent: The test suite as a parent element in the test entity hierarchy. 

453 :param testcaseNode: The current XML element node representing a test case. 

454 """ 

455 className = self._ConvertClassname(testcaseNode) 

456 if className == "": 

457 testcaseParent = parent 

458 else: 

459 testcaseParent = self._FindOrCreateTestclass(parent, className) 

460 

461 newTestcase = self._TESTCASE( 

462 self._ConvertName(testcaseNode, optional=False), 

463 self._ConvertTime(testcaseNode, optional=False), 

464 startTime=self._ConvertTimestamp(testcaseNode, optional=True), 

465 rerunCount=len(tuple(testcaseNode.iterchildren("flakyFailure", "flakyError", "rerunFailure", "rerunError"))), 

466 parent=testcaseParent 

467 ) 

468 

469 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

470 

471 def Generate(self, overwrite: bool = False) -> None: 

472 """ 

473 Generate the internal XML data structure from test suites and test cases. 

474 

475 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods. 

476 

477 :param overwrite: Optional, overwrite the internal XML data structure. 

478 :raises UnittestError: If overwrite is false and the internal XML data structure is not empty. 

479 :raises UnittestError: If the test suite summary has no start time. 

480 :raises UnittestError: If the test suite summary has no duration. 

481 """ 

482 if not overwrite and self._xmlDocument is not None: 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 raise UnittestError("Internal XML document is populated with data.") 

484 

485 if self._startTime is None: 

486 raise UnittestError( 

487 f"The {self._DIALECT} format requires a timestamp on <testsuites>, but the report has none." 

488 ) 

489 

490 if self._duration is None: 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true

491 raise UnittestError(f"The {self._DIALECT} format requires a time on <testsuites>, but the report has none.") 

492 

493 rootElement = Element("testsuites") 

494 rootElement.attrib["name"] = self._name 

495 rootElement.attrib["tests"] = str(self._tests) 

496 rootElement.attrib["skipped"] = str(self._skipped) 

497 rootElement.attrib["failures"] = str(self._failed) 

498 rootElement.attrib["errors"] = str(self._errored) 

499 if self._runID is not None: 

500 rootElement.attrib["uuid"] = str(self._runID) 

501 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}" 

502 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}" 

503 

504 self._xmlDocument = ElementTree(rootElement) 

505 

506 for testsuite in self._testsuites.values(): 

507 self._GenerateTestsuite(testsuite, rootElement) 

508 

509 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None: 

510 """ 

511 Generate the internal XML data structure for a test suite. 

512 

513 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods. The 

514 dialect has no hostname, start time and duration on a ``<testsuite>``, so these are not written. 

515 

516 :param testsuite: The test suite to convert to an XML data structures. 

517 :param parentElement: The parent XML data structure element, this data structure part will be added to. 

518 """ 

519 testsuiteElement = SubElement(parentElement, "testsuite") 

520 testsuiteElement.attrib["name"] = testsuite._name 

521 testsuiteElement.attrib["tests"] = str(testsuite._tests) 

522 testsuiteElement.attrib["skipped"] = str(testsuite._skipped) 

523 testsuiteElement.attrib["errors"] = str(testsuite._errored) 

524 testsuiteElement.attrib["failures"] = str(testsuite._failed) 

525 

526 for tc in testsuite._testcases.values(): 

527 self._GenerateTestcase(tc, testsuiteElement) 

528 

529 for testclass in testsuite._testclasses.values(): 

530 for tc in testclass._testcases.values(): 

531 self._GenerateTestcase(tc, testsuiteElement) 

532 

533 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None: 

534 """ 

535 Generate the internal XML data structure for a test case. 

536 

537 This method generates the XML element (``<testcase>``) and recursively calls other generated methods. Reruns are 

538 not written. 

539 

540 :param testcase: The test case to convert to an XML data structures. 

541 :param parentElement: The parent XML data structure element, this data structure part will be added to. 

542 :raises UnittestError: If the test case has no duration. 

543 """ 

544 if testcase._duration is None: 544 ↛ 545line 544 didn't jump to line 545 because the condition on line 544 was never true

545 raise UnittestError( 

546 f"The {self._DIALECT} format requires a time on <testcase>, but test case '{testcase._name}' has none." 

547 ) 

548 

549 testcaseElement = SubElement(parentElement, "testcase") 

550 testcaseElement.attrib["name"] = testcase._name 

551 testcaseElement.attrib["classname"] = testcase.Classname 

552 if testcase._startTime is not None: 

553 testcaseElement.attrib["timestamp"] = f"{testcase._startTime.isoformat()}" 

554 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}" 

555 

556 self._GenerateTestcaseChildren(testcase, testcaseElement)