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

192 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:49 +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 

52from pyEDAA.Reports.Unittesting import TestsuiteSummary as ut_TestsuiteSummary, Testsuite as ut_Testsuite 

53from pyEDAA.Reports.Unittesting import Testcase as ut_Testcase 

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

55from pyEDAA.Reports.Unittesting.JUnit import Testsuite as ju_Testsuite 

56from pyEDAA.Reports.Unittesting.JUnit import TestsuiteSummary as ju_TestsuiteSummary, Document as ju_Document 

57from pyEDAA.Reports.Unittesting.JUnit import JUnitReaderMode 

58 

59 

60@export 

61@InheritDocString(ju_Testcase, DocStringMergeStrategy.BaseLast) 

62class Testcase(ju_Testcase): 

63 """ 

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

65 

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

67 nextest recorded for it. 

68 """ 

69 

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

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

72 

73 def __init__( 

74 self, 

75 name: str, 

76 duration: Nullable[timedelta] = None, 

77 status: TestcaseStatus = TestcaseStatus.Unknown, 

78 assertionCount: Nullable[int] = None, 

79 message: Nullable[str] = None, 

80 details: Nullable[str] = None, 

81 standardOutput: Nullable[str] = None, 

82 standardError: Nullable[str] = None, 

83 startTime: Nullable[datetime] = None, 

84 rerunCount: int = 0, 

85 *, 

86 parent: Nullable[Testclass] = None 

87 ) -> None: 

88 """ 

89 Initializes the fields of a test case. 

90 

91 :param name: Name of the test entity. 

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

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

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

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

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

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

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

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

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

101 :param parent: Optional, reference to the parent test class. 

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

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

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

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

106 """ 

107 super().__init__( 

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

109 ) 

110 

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

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

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

114 raise ex 

115 

116 if rerunCount is None: 

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

118 elif not isinstance(rerunCount, int): 

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

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

121 raise ex 

122 elif rerunCount < 0: 

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

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

125 raise ex 

126 

127 self._startTime = startTime 

128 self._rerunCount = rerunCount 

129 

130 @readonly 

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

132 """ 

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

134 

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

136 

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

138 """ 

139 return self._startTime 

140 

141 @readonly 

142 def RerunCount(self) -> int: 

143 """ 

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

145 

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

147 

148 :returns: Number of reruns. 

149 """ 

150 return self._rerunCount 

151 

152 def Copy(self) -> Testcase: 

153 """ 

154 Copy the test case without its parent. 

155 

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

157 """ 

158 testcase = super().Copy() 

159 testcase._startTime = self._startTime 

160 testcase._rerunCount = self._rerunCount 

161 

162 return testcase 

163 

164 @classmethod 

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

166 """ 

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

168 adhering to the cargo-nextest JUnit dialect. 

169 

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

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

172 """ 

173 juTestcase = super().FromTestcase(testcase) 

174 juTestcase._startTime = testcase._startTime 

175 

176 return juTestcase 

177 

178 def ToTestcase(self) -> ut_Testcase: 

179 """ 

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

181 

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

183 

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

185 """ 

186 testcase = super().ToTestcase() 

187 testcase._startTime = self._startTime 

188 

189 return testcase 

190 

191 

192@export 

193@InheritDocString(ju_Testclass, DocStringMergeStrategy.BaseLast) 

194class Testclass(ju_Testclass): 

195 """ 

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

197 """ 

198 

199 

200@export 

201@InheritDocString(ju_Testsuite, DocStringMergeStrategy.BaseLast) 

202class Testsuite(ju_Testsuite): 

203 """ 

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

205 """ 

206 

207 @classmethod 

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

209 """ 

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

211 adhering to the cargo-nextest JUnit dialect. 

212 

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

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

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

216 """ 

217 juTestsuite = cls( 

218 testsuite._name, 

219 hostname=testsuite._hostname, 

220 startTime=testsuite._startTime, 

221 duration=testsuite._totalDuration, 

222 status= testsuite._status, 

223 ) 

224 

225 juTestsuite._tests = testsuite._tests 

226 juTestsuite._skipped = testsuite._skipped 

227 juTestsuite._errored = testsuite._errored 

228 juTestsuite._failed = testsuite._failed 

229 juTestsuite._passed = testsuite._passed 

230 

231 for tc in testsuite.IterateTestcases(): 

232 ts = tc._parent 

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

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

235 

236 classname = ts._name 

237 ts = ts._parent 

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

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

240 ts = ts._parent 

241 

242 if classname in juTestsuite._testclasses: 

243 juClass = juTestsuite._testclasses[classname] 

244 else: 

245 juClass = Testclass(classname, parent=juTestsuite) 

246 

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

248 

249 return juTestsuite 

250 

251 

252@export 

253@InheritDocString(ju_TestsuiteSummary, DocStringMergeStrategy.BaseLast) 

254class TestsuiteSummary(ju_TestsuiteSummary): 

255 """ 

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

257 """ 

258 

259 @classmethod 

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

261 """ 

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

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

264 

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

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

267 """ 

268 return cls( 

269 testsuiteSummary._name, 

270 startTime=testsuiteSummary._startTime, 

271 duration=testsuiteSummary._totalDuration, 

272 status=testsuiteSummary._status, 

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

274 ) 

275 

276 

277@export 

278class Document(ju_Document): 

279 """ 

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

281 

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

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

284 

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

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

287 """ 

288 

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

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

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

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

293 

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

295 

296 def __init__( 

297 self, 

298 xmlReportFile: Path, 

299 analyzeAndConvert: bool = False, 

300 readerMode: JUnitReaderMode = JUnitReaderMode.Default, 

301 *, 

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

303 startTime: Nullable[datetime] = None, 

304 duration: Nullable[timedelta] = None, 

305 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

307 runID: Nullable[UUID] = None 

308 ) -> None: 

309 """ 

310 Initializes a cargo-nextest JUnit document. 

311 

312 :param xmlReportFile: Path to the XML file. 

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

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

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

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

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

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

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

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

321 """ 

322 super().__init__( 

323 xmlReportFile, 

324 False, 

325 readerMode, 

326 name=name, 

327 startTime=startTime, 

328 duration=duration, 

329 status=status, 

330 testsuites=testsuites 

331 ) 

332 

333 self._runID = runID 

334 

335 if analyzeAndConvert: 

336 self.Analyze() 

337 self.Convert() 

338 

339 @readonly 

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

341 """ 

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

343 

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

345 """ 

346 return self._runID 

347 

348 def Analyze(self) -> None: 

349 """ 

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

351 schema. 

352 

353 .. hint:: 

354 

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

356 

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

358 """ 

359 xmlSchemaFile = "Nextest-JUnit.xsd" 

360 self._Analyze(xmlSchemaFile) 

361 

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

363 """ 

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

365 

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

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

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

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

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

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

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

373 """ 

374 if path is None: 

375 path = self._path 

376 

377 if not overwrite and path.exists(): 

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

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

380 

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

382 self.Generate(overwrite=True) 

383 

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

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

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

387 raise ex 

388 

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

390 try: 

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

392 file.write(content) 

393 except OSError as ex: 

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

395 

396 def Convert(self) -> None: 

397 """ 

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

399 

400 This method converts the root element. 

401 

402 .. hint:: 

403 

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

405 

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

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

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

409 """ 

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

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

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

413 raise ex 

414 

415 with Stopwatch() as sw: 

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

417 

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

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

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

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

422 

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

424 self._ConvertTestsuite(self, rootNode) 

425 

426 self.Aggregate() 

427 

428 self._modelConversion = sw.Duration 

429 

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

431 """ 

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

433 

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

435 

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

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

438 """ 

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

440 

441 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

442 

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

444 """ 

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

446 

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

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

449 

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

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

452 """ 

453 className = self._ConvertClassname(testcaseNode) 

454 testclass = self._FindOrCreateTestclass(parent, className) 

455 

456 newTestcase = self._TESTCASE( 

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

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

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

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

461 parent=testclass 

462 ) 

463 

464 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

465 

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

467 """ 

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

469 

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

471 

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

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

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

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

476 """ 

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

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

479 

480 if self._startTime is None: 

481 raise UnittestError( 

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

483 ) 

484 

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

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

487 

488 rootElement = Element("testsuites") 

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

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

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

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

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

494 if self._runID is not None: 

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

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

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

498 

499 self._xmlDocument = ElementTree(rootElement) 

500 

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

502 self._GenerateTestsuite(testsuite, rootElement) 

503 

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

505 """ 

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

507 

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

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

510 

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

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

513 """ 

514 testsuiteElement = SubElement(parentElement, "testsuite") 

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

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

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

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

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

520 

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

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

523 self._GenerateTestcase(tc, testsuiteElement) 

524 

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

526 """ 

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

528 

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

530 not written. 

531 

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

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

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

535 """ 

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

537 raise UnittestError( 

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

539 ) 

540 

541 testcaseElement = SubElement(parentElement, "testcase") 

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

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

544 if testcase._startTime is not None: 

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

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

547 

548 self._GenerateTestcaseChildren(testcase, testcaseElement)