Coverage for pyEDAA/Reports/Unittesting/__init__.py: 78%

798 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-07 22:48 +0000

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

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

32The pyEDAA.Reports.Unittesting package implements a hierarchy of test entities. These are test cases, test suites and a 

33test summary provided as a class hierarchy. Test cases are the leaf elements in the hierarchy and abstract an 

34individual test run. Test suites are used to group multiple test cases or other test suites. The root element is a test 

35summary. When such a summary is stored in a file format like Ant + JUnit4 XML, a file format specific document is 

36derived from a summary class. 

37 

38**Data Model** 

39 

40.. mermaid:: 

41 

42 graph TD; 

43 doc[Document] 

44 sum[Summary] 

45 ts1[Testsuite] 

46 ts2[Testsuite] 

47 ts21[Testsuite] 

48 tc11[Testcase] 

49 tc12[Testcase] 

50 tc13[Testcase] 

51 tc21[Testcase] 

52 tc22[Testcase] 

53 tc211[Testcase] 

54 tc212[Testcase] 

55 tc213[Testcase] 

56 

57 doc:::root -.-> sum:::summary 

58 sum --> ts1:::suite 

59 sum --> ts2:::suite 

60 ts2 --> ts21:::suite 

61 ts1 --> tc11:::case 

62 ts1 --> tc12:::case 

63 ts1 --> tc13:::case 

64 ts2 --> tc21:::case 

65 ts2 --> tc22:::case 

66 ts21 --> tc211:::case 

67 ts21 --> tc212:::case 

68 ts21 --> tc213:::case 

69 

70 classDef root fill:#4dc3ff 

71 classDef summary fill:#80d4ff 

72 classDef suite fill:#b3e6ff 

73 classDef case fill:#eeccff 

74""" 

75from datetime import timedelta, datetime 

76from enum import Flag, IntEnum 

77from pathlib import Path 

78from sys import version_info 

79from typing import Optional as Nullable, Dict, Iterable, Any, Tuple, Generator, Union, List, Generic, TypeVar, Mapping 

80 

81from pyTooling.Common import getFullyQualifiedName 

82from pyTooling.Decorators import export, readonly 

83from pyTooling.MetaClasses import ExtendedType, abstractmethod 

84from pyTooling.Tree import Node 

85 

86from pyEDAA.Reports import ReportException 

87 

88 

89@export 

90class UnittestException(ReportException): 

91 """Base-exception for all unit test related exceptions.""" 

92 

93 

94@export 

95class AlreadyInHierarchyException(UnittestException): 

96 """ 

97 A unit test exception raised if the element is already part of a hierarchy. 

98 

99 This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same 

100 hierarchy should occur only once in the hierarchy. 

101 

102 .. hint:: 

103 

104 This is usually caused by a non-None parent reference. 

105 """ 

106 

107 

108@export 

109class DuplicateTestsuiteException(UnittestException): 

110 """ 

111 A unit test exception raised on duplicate test suites (by name). 

112 

113 This exception is raised, if a child test suite with same name already exist in the test suite. 

114 

115 .. hint:: 

116 

117 Test suite names need to be unique per parent element (test suite or test summary). 

118 """ 

119 

120 

121@export 

122class DuplicateTestcaseException(UnittestException): 

123 """ 

124 A unit test exception raised on duplicate test cases (by name). 

125 

126 This exception is raised, if a child test case with same name already exist in the test suite. 

127 

128 .. hint:: 

129 

130 Test case names need to be unique per parent element (test suite). 

131 """ 

132 

133 

134@export 

135class TestcaseStatus(Flag): 

136 """A flag enumeration describing the status of a test case.""" 

137 Unknown = 0 #: Testcase status is uninitialized and therefore unknown. 

138 Excluded = 1 #: Testcase was permanently excluded / disabled 

139 Skipped = 2 #: Testcase was temporarily skipped (e.g. based on a condition) 

140 Weak = 4 #: No assertions were recorded. 

141 Passed = 8 #: A passed testcase, because all assertions were successful. 

142 Failed = 16 #: A failed testcase due to at least one failed assertion. 

143 

144 Mask = Excluded | Skipped | Weak | Passed | Failed 

145 

146 Inverted = 128 #: To mark inverted results 

147 UnexpectedPassed = Failed | Inverted 

148 ExpectedFailed = Passed | Inverted 

149 

150 Warned = 1024 #: Runtime warning 

151 Errored = 2048 #: Runtime error (mostly caught exceptions) 

152 Aborted = 4096 #: Uncaught runtime exception 

153 

154 SetupError = 8192 #: Preparation / compilation error 

155 TearDownError = 16384 #: Cleanup error / resource release error 

156 Inconsistent = 32768 #: Dataset is inconsistent 

157 

158 Flags = Warned | Errored | Aborted | SetupError | TearDownError | Inconsistent 

159 

160 # TODO: timed out ? 

161 # TODO: some passed (if merged, mixed results of passed and failed) 

162 

163 def __matmul__(self, other: "TestcaseStatus") -> "TestcaseStatus": 

164 s = self & self.Mask 

165 o = other & self.Mask 

166 if s is self.Excluded: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true

167 resolved = self.Excluded if o is self.Excluded else self.Unknown 

168 elif s is self.Skipped: 

169 resolved = self.Unknown if (o is self.Unknown) or (o is self.Excluded) else o 

170 elif s is self.Weak: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true

171 resolved = self.Weak if o is self.Weak else self.Unknown 

172 elif s is self.Passed: 172 ↛ 177line 172 didn't jump to line 177 because the condition on line 172 was always true

173 if o is self.Failed: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 resolved = self.Failed 

175 else: 

176 resolved = self.Passed if (o is self.Skipped) or (o is self.Passed) else self.Unknown 

177 elif s is self.Failed: 

178 resolved = self.Failed if (o is self.Skipped) or (o is self.Passed) or (o is self.Failed) else self.Unknown 

179 else: 

180 resolved = self.Unknown 

181 

182 resolved |= (self & self.Flags) | (other & self.Flags) 

183 return resolved 

184 

185 

186@export 

187class TestsuiteStatus(Flag): 

188 """A flag enumeration describing the status of a test suite.""" 

189 Unknown = 0 

190 Excluded = 1 #: Testcase was permanently excluded / disabled 

191 Skipped = 2 #: Testcase was temporarily skipped (e.g. based on a condition) 

192 Empty = 4 #: No tests in suite 

193 Passed = 8 #: Passed testcase, because all assertions succeeded 

194 Failed = 16 #: Failed testcase due to failing assertions 

195 

196 Mask = Excluded | Skipped | Empty | Passed | Failed 

197 

198 Inverted = 128 #: To mark inverted results 

199 UnexpectedPassed = Failed | Inverted 

200 ExpectedFailed = Passed | Inverted 

201 

202 Warned = 1024 #: Runtime warning 

203 Errored = 2048 #: Runtime error (mostly caught exceptions) 

204 Aborted = 4096 #: Uncaught runtime exception 

205 

206 SetupError = 8192 #: Preparation / compilation error 

207 TearDownError = 16384 #: Cleanup error / resource release error 

208 

209 Flags = Warned | Errored | Aborted | SetupError | TearDownError 

210 

211 

212@export 

213class TestsuiteKind(IntEnum): 

214 """Enumeration describing the kind of test suite.""" 

215 Root = 0 #: Root element of the hierarchy. 

216 Logical = 1 #: Represents a logical unit. 

217 Namespace = 2 #: Represents a namespace. 

218 Package = 3 #: Represents a package. 

219 Module = 4 #: Represents a module. 

220 Class = 5 #: Represents a class. 

221 

222 

223@export 

224class IterationScheme(Flag): 

225 """ 

226 A flag enumeration for selecting the test suite iteration scheme. 

227 

228 When a test entity hierarchy is (recursively) iterated, this iteration scheme describes how to iterate the hierarchy 

229 and what elements to return as a result. 

230 """ 

231 Unknown = 0 #: Neutral element. 

232 IncludeSelf = 1 #: Also include the element itself. 

233 IncludeTestsuites = 2 #: Include test suites into the result. 

234 IncludeTestcases = 4 #: Include test cases into the result. 

235 

236 Recursive = 8 #: Iterate recursively. 

237 

238 PreOrder = 16 #: Iterate in pre-order (top-down: current node, then child element left-to-right). 

239 PostOrder = 32 #: Iterate in pre-order (bottom-up: child element left-to-right, then current node). 

240 

241 Default = IncludeTestsuites | Recursive | IncludeTestcases | PreOrder #: Recursively iterate all test entities in pre-order. 

242 TestsuiteDefault = IncludeTestsuites | Recursive | PreOrder #: Recursively iterate only test suites in pre-order. 

243 TestcaseDefault = IncludeTestcases | Recursive | PreOrder #: Recursively iterate only test cases in pre-order. 

244 

245 

246TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite") 

247TestcaseAggregateReturnType = Tuple[int, int, int, int, int, int, timedelta] 

248TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int, int, int, int, int, int, int, int, int, timedelta] 

249 

250 

251@export 

252class Base(metaclass=ExtendedType, slots=True): 

253 """ 

254 Base-class for all test entities (test cases, test suites, ...). 

255 

256 It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity 

257 hierarchy. 

258 

259 Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the 

260 child. |br| 

261 E.g. it's used as a test case name in the dictionary of test cases in a test suite. 

262 

263 Every test entity has fields for time tracking. If known, a start time and a test duration can be set. For more 

264 details, a setup duration and teardown duration can be added. All durations are summed up in a total duration field. 

265 

266 As tests can have warnings and errors or even fail, these messages are counted and aggregated in the test entity 

267 hierarchy. 

268 

269 Every test entity offers an internal dictionary for annotations. |br| 

270 This feature is for example used by Ant + JUnit4's XML property fields. 

271 """ 

272 

273 _parent: Nullable["TestsuiteBase"] 

274 _name: str 

275 

276 _startTime: Nullable[datetime] 

277 _setupDuration: Nullable[timedelta] 

278 _testDuration: Nullable[timedelta] 

279 _teardownDuration: Nullable[timedelta] 

280 _totalDuration: Nullable[timedelta] 

281 

282 _warningCount: int 

283 _errorCount: int 

284 _fatalCount: int 

285 

286 _expectedWarningCount: int 

287 _expectedErrorCount: int 

288 _expectedFatalCount: int 

289 

290 _dict: Dict[str, Any] 

291 

292 def __init__( 

293 self, 

294 name: str, 

295 startTime: Nullable[datetime] = None, 

296 setupDuration: Nullable[timedelta] = None, 

297 testDuration: Nullable[timedelta] = None, 

298 teardownDuration: Nullable[timedelta] = None, 

299 totalDuration: Nullable[timedelta] = None, 

300 warningCount: int = 0, 

301 errorCount: int = 0, 

302 fatalCount: int = 0, 

303 expectedWarningCount: int = 0, 

304 expectedErrorCount: int = 0, 

305 expectedFatalCount: int = 0, 

306 keyValuePairs: Nullable[Mapping[str, Any]] = None, 

307 parent: Nullable["TestsuiteBase"] = None 

308 ) -> None: 

309 """ 

310 Initializes the fields of the base-class. 

311 

312 :param name: Name of the test entity. 

313 :param startTime: Time when the test entity was started. 

314 :param setupDuration: Duration it took to set up the entity. 

315 :param testDuration: Duration of the entity's test run. 

316 :param teardownDuration: Duration it took to tear down the entity. 

317 :param totalDuration: Total duration of the entity's execution (setup + test + teardown). 

318 :param warningCount: Count of encountered warnings. 

319 :param errorCount: Count of encountered errors. 

320 :param fatalCount: Count of encountered fatal errors. 

321 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with. 

322 :param parent: Reference to the parent test entity. 

323 :raises TypeError: When parameter 'parent' is not a TestsuiteBase. 

324 :raises ValueError: When parameter 'name' is None. 

325 :raises TypeError: When parameter 'name' is not a string. 

326 :raises ValueError: When parameter 'name' is empty. 

327 :raises TypeError: When parameter 'testDuration' is not a timedelta. 

328 :raises TypeError: When parameter 'setupDuration' is not a timedelta. 

329 :raises TypeError: When parameter 'teardownDuration' is not a timedelta. 

330 :raises TypeError: When parameter 'totalDuration' is not a timedelta. 

331 :raises TypeError: When parameter 'warningCount' is not an integer. 

332 :raises TypeError: When parameter 'errorCount' is not an integer. 

333 :raises TypeError: When parameter 'fatalCount' is not an integer. 

334 :raises TypeError: When parameter 'expectedWarningCount' is not an integer. 

335 :raises TypeError: When parameter 'expectedErrorCount' is not an integer. 

336 :raises TypeError: When parameter 'expectedFatalCount' is not an integer. 

337 :raises TypeError: When parameter 'keyValuePairs' is not a Mapping. 

338 :raises ValueError: When parameter 'totalDuration' is not consistent. 

339 """ 

340 

341 if parent is not None and not isinstance(parent, TestsuiteBase): 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true

342 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.") 

343 if version_info >= (3, 11): # pragma: no cover 

344 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

345 raise ex 

346 

347 if name is None: 

348 raise ValueError(f"Parameter 'name' is None.") 

349 elif not isinstance(name, str): 349 ↛ 350line 349 didn't jump to line 350 because the condition on line 349 was never true

350 ex = TypeError(f"Parameter 'name' is not of type 'str'.") 

351 if version_info >= (3, 11): # pragma: no cover 

352 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

353 raise ex 

354 elif name.strip() == "": 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true

355 raise ValueError(f"Parameter 'name' is empty.") 

356 

357 self._parent = parent 

358 self._name = name 

359 

360 if testDuration is not None and not isinstance(testDuration, timedelta): 360 ↛ 361line 360 didn't jump to line 361 because the condition on line 360 was never true

361 ex = TypeError(f"Parameter 'testDuration' is not of type 'timedelta'.") 

362 if version_info >= (3, 11): # pragma: no cover 

363 ex.add_note(f"Got type '{getFullyQualifiedName(testDuration)}'.") 

364 raise ex 

365 

366 if setupDuration is not None and not isinstance(setupDuration, timedelta): 366 ↛ 367line 366 didn't jump to line 367 because the condition on line 366 was never true

367 ex = TypeError(f"Parameter 'setupDuration' is not of type 'timedelta'.") 

368 if version_info >= (3, 11): # pragma: no cover 

369 ex.add_note(f"Got type '{getFullyQualifiedName(setupDuration)}'.") 

370 raise ex 

371 

372 if teardownDuration is not None and not isinstance(teardownDuration, timedelta): 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true

373 ex = TypeError(f"Parameter 'teardownDuration' is not of type 'timedelta'.") 

374 if version_info >= (3, 11): # pragma: no cover 

375 ex.add_note(f"Got type '{getFullyQualifiedName(teardownDuration)}'.") 

376 raise ex 

377 

378 if totalDuration is not None and not isinstance(totalDuration, timedelta): 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true

379 ex = TypeError(f"Parameter 'totalDuration' is not of type 'timedelta'.") 

380 if version_info >= (3, 11): # pragma: no cover 

381 ex.add_note(f"Got type '{getFullyQualifiedName(totalDuration)}'.") 

382 raise ex 

383 

384 if testDuration is not None: 

385 if setupDuration is not None: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true

386 if teardownDuration is not None: 

387 if totalDuration is not None: 

388 if totalDuration < (setupDuration + testDuration + teardownDuration): 

389 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup, test and teardown durations.") 

390 else: # no total 

391 totalDuration = setupDuration + testDuration + teardownDuration 

392 # no teardown 

393 elif totalDuration is not None: 

394 if totalDuration < (setupDuration + testDuration): 

395 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup and test durations.") 

396 # no teardown, no total 

397 else: 

398 totalDuration = setupDuration + testDuration 

399 # no setup 

400 elif teardownDuration is not None: 400 ↛ 401line 400 didn't jump to line 401 because the condition on line 400 was never true

401 if totalDuration is not None: 

402 if totalDuration < (testDuration + teardownDuration): 

403 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of test and teardown durations.") 

404 else: # no setup, no total 

405 totalDuration = testDuration + teardownDuration 

406 # no setup, no teardown 

407 elif totalDuration is not None: 

408 if totalDuration < testDuration: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true

409 raise ValueError(f"Parameter 'totalDuration' can not be less than test durations.") 

410 else: # no setup, no teardown, no total 

411 totalDuration = testDuration 

412 # no test 

413 elif totalDuration is not None: 

414 testDuration = totalDuration 

415 if setupDuration is not None: 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true

416 testDuration -= setupDuration 

417 if teardownDuration is not None: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true

418 testDuration -= teardownDuration 

419 

420 self._startTime = startTime 

421 self._setupDuration = setupDuration 

422 self._testDuration = testDuration 

423 self._teardownDuration = teardownDuration 

424 self._totalDuration = totalDuration 

425 

426 if not isinstance(warningCount, int): 426 ↛ 427line 426 didn't jump to line 427 because the condition on line 426 was never true

427 ex = TypeError(f"Parameter 'warningCount' is not of type 'int'.") 

428 if version_info >= (3, 11): # pragma: no cover 

429 ex.add_note(f"Got type '{getFullyQualifiedName(warningCount)}'.") 

430 raise ex 

431 

432 if not isinstance(errorCount, int): 432 ↛ 433line 432 didn't jump to line 433 because the condition on line 432 was never true

433 ex = TypeError(f"Parameter 'errorCount' is not of type 'int'.") 

434 if version_info >= (3, 11): # pragma: no cover 

435 ex.add_note(f"Got type '{getFullyQualifiedName(errorCount)}'.") 

436 raise ex 

437 

438 if not isinstance(fatalCount, int): 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true

439 ex = TypeError(f"Parameter 'fatalCount' is not of type 'int'.") 

440 if version_info >= (3, 11): # pragma: no cover 

441 ex.add_note(f"Got type '{getFullyQualifiedName(fatalCount)}'.") 

442 raise ex 

443 

444 if not isinstance(expectedWarningCount, int): 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true

445 ex = TypeError(f"Parameter 'expectedWarningCount' is not of type 'int'.") 

446 if version_info >= (3, 11): # pragma: no cover 

447 ex.add_note(f"Got type '{getFullyQualifiedName(expectedWarningCount)}'.") 

448 raise ex 

449 

450 if not isinstance(expectedErrorCount, int): 450 ↛ 451line 450 didn't jump to line 451 because the condition on line 450 was never true

451 ex = TypeError(f"Parameter 'expectedErrorCount' is not of type 'int'.") 

452 if version_info >= (3, 11): # pragma: no cover 

453 ex.add_note(f"Got type '{getFullyQualifiedName(expectedErrorCount)}'.") 

454 raise ex 

455 

456 if not isinstance(expectedFatalCount, int): 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true

457 ex = TypeError(f"Parameter 'expectedFatalCount' is not of type 'int'.") 

458 if version_info >= (3, 11): # pragma: no cover 

459 ex.add_note(f"Got type '{getFullyQualifiedName(expectedFatalCount)}'.") 

460 raise ex 

461 

462 self._warningCount = warningCount 

463 self._errorCount = errorCount 

464 self._fatalCount = fatalCount 

465 self._expectedWarningCount = expectedWarningCount 

466 self._expectedErrorCount = expectedErrorCount 

467 self._expectedFatalCount = expectedFatalCount 

468 

469 if keyValuePairs is not None and not isinstance(keyValuePairs, Mapping): 469 ↛ 470line 469 didn't jump to line 470 because the condition on line 469 was never true

470 ex = TypeError(f"Parameter 'keyValuePairs' is not a mapping.") 

471 if version_info >= (3, 11): # pragma: no cover 

472 ex.add_note(f"Got type '{getFullyQualifiedName(keyValuePairs)}'.") 

473 raise ex 

474 

475 self._dict = {} if keyValuePairs is None else {k: v for k, v in keyValuePairs} 

476 

477 # QUESTION: allow Parent as setter? 

478 @readonly 

479 def Parent(self) -> Nullable["TestsuiteBase"]: 

480 """ 

481 Read-only property to access the reference to the parent test entity. 

482 

483 :returns: Reference to the parent entity. 

484 """ 

485 return self._parent 

486 

487 @readonly 

488 def Name(self) -> str: 

489 """ 

490 Read-only property to access the test entity's name. 

491 

492 :returns: The test entities name. 

493 """ 

494 return self._name 

495 

496 @readonly 

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

498 """ 

499 Read-only property to access the time when the test entity was started. 

500 

501 :returns: Time when the test entity was started. 

502 """ 

503 return self._startTime 

504 

505 @readonly 

506 def SetupDuration(self) -> Nullable[timedelta]: 

507 """ 

508 Read-only property to access the duration of the test entity's setup. 

509 

510 :returns: Duration it took to set up the entity. 

511 """ 

512 return self._setupDuration 

513 

514 @readonly 

515 def TestDuration(self) -> Nullable[timedelta]: 

516 """ 

517 Read-only property to access the duration of a test entities run. 

518 

519 This duration is excluding setup and teardown durations. In case setup and/or teardown durations are unknown or not 

520 distinguishable, assign setup and teardown durations with zero. 

521 

522 :returns: Duration of the entity's test run. 

523 """ 

524 return self._testDuration 

525 

526 @readonly 

527 def TeardownDuration(self) -> Nullable[timedelta]: 

528 """ 

529 Read-only property to access the duration of the test entity's teardown. 

530 

531 :returns: Duration it took to tear down the entity. 

532 """ 

533 return self._teardownDuration 

534 

535 @readonly 

536 def TotalDuration(self) -> Nullable[timedelta]: 

537 """ 

538 Read-only property to access the total duration of a test entity run. 

539 

540 this duration includes setup and teardown durations. 

541 

542 :returns: Total duration of the entity's execution (setup + test + teardown) 

543 """ 

544 return self._totalDuration 

545 

546 @readonly 

547 def WarningCount(self) -> int: 

548 """ 

549 Read-only property to access the number of encountered warnings. 

550 

551 :returns: Count of encountered warnings. 

552 """ 

553 return self._warningCount 

554 

555 @readonly 

556 def ErrorCount(self) -> int: 

557 """ 

558 Read-only property to access the number of encountered errors. 

559 

560 :returns: Count of encountered errors. 

561 """ 

562 return self._errorCount 

563 

564 @readonly 

565 def FatalCount(self) -> int: 

566 """ 

567 Read-only property to access the number of encountered fatal errors. 

568 

569 :returns: Count of encountered fatal errors. 

570 """ 

571 return self._fatalCount 

572 

573 @readonly 

574 def ExpectedWarningCount(self) -> int: 

575 """ 

576 Read-only property to access the number of expected warnings. 

577 

578 :returns: Count of expected warnings. 

579 """ 

580 return self._expectedWarningCount 

581 

582 @readonly 

583 def ExpectedErrorCount(self) -> int: 

584 """ 

585 Read-only property to access the number of expected errors. 

586 

587 :returns: Count of expected errors. 

588 """ 

589 return self._expectedErrorCount 

590 

591 @readonly 

592 def ExpectedFatalCount(self) -> int: 

593 """ 

594 Read-only property to access the number of expected fatal errors. 

595 

596 :returns: Count of expected fatal errors. 

597 """ 

598 return self._expectedFatalCount 

599 

600 def __len__(self) -> int: 

601 """ 

602 Returns the number of annotated key-value pairs. 

603 

604 :returns: Number of annotated key-value pairs. 

605 """ 

606 return len(self._dict) 

607 

608 def __getitem__(self, key: str) -> Any: 

609 """ 

610 Access a key-value pair by key. 

611 

612 :param key: Name if the key-value pair. 

613 :returns: Value of the accessed key. 

614 """ 

615 return self._dict[key] 

616 

617 def __setitem__(self, key: str, value: Any) -> None: 

618 """ 

619 Set the value of a key-value pair by key. 

620 

621 If the pair doesn't exist yet, it's created. 

622 

623 :param key: Key of the key-value pair. 

624 :param value: Value of the key-value pair. 

625 """ 

626 self._dict[key] = value 

627 

628 def __delitem__(self, key: str) -> None: 

629 """ 

630 Delete a key-value pair by key. 

631 

632 :param key: Name if the key-value pair. 

633 """ 

634 del self._dict[key] 

635 

636 def __contains__(self, key: str) -> bool: 

637 """ 

638 Returns True, if a key-value pairs was annotated by this key. 

639 

640 :param key: Name of the key-value pair. 

641 :returns: True, if the pair was annotated. 

642 """ 

643 return key in self._dict 

644 

645 def __iter__(self) -> Generator[Tuple[str, Any], None, None]: 

646 """ 

647 Iterate all annotated key-value pairs. 

648 

649 :returns: A generator of key-value pair tuples (key, value). 

650 """ 

651 yield from self._dict.items() 

652 

653 @abstractmethod 

654 def Aggregate(self, strict: bool = True) -> None: 

655 """ 

656 Aggregate all test entities in the hierarchy. 

657 """ 

658 

659 @abstractmethod 

660 def __str__(self) -> str: 

661 """ 

662 Formats the test entity as human-readable incl. some statistics. 

663 """ 

664 

665 

666@export 

667class Testcase(Base): 

668 """ 

669 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run. 

670 

671 Test cases are grouped by test suites in the test entity hierarchy. The root of the hierarchy is a test summary. 

672 

673 Every test case has an overall status like unknown, skipped, failed or passed. 

674 

675 In addition to all features from its base-class, test cases provide additional statistics for passed and failed 

676 assertions (checks) as well as a sum thereof. 

677 """ 

678 

679 _status: TestcaseStatus 

680 _assertionCount: Nullable[int] 

681 _failedAssertionCount: Nullable[int] 

682 _passedAssertionCount: Nullable[int] 

683 

684 def __init__( 

685 self, 

686 name: str, 

687 startTime: Nullable[datetime] = None, 

688 setupDuration: Nullable[timedelta] = None, 

689 testDuration: Nullable[timedelta] = None, 

690 teardownDuration: Nullable[timedelta] = None, 

691 totalDuration: Nullable[timedelta] = None, 

692 status: TestcaseStatus = TestcaseStatus.Unknown, 

693 assertionCount: Nullable[int] = None, 

694 failedAssertionCount: Nullable[int] = None, 

695 passedAssertionCount: Nullable[int] = None, 

696 warningCount: int = 0, 

697 errorCount: int = 0, 

698 fatalCount: int = 0, 

699 expectedWarningCount: int = 0, 

700 expectedErrorCount: int = 0, 

701 expectedFatalCount: int = 0, 

702 keyValuePairs: Nullable[Mapping[str, Any]] = None, 

703 parent: Nullable["Testsuite"] = None 

704 ) -> None: 

705 """ 

706 Initializes the fields of a test case. 

707 

708 :param name: Name of the test entity. 

709 :param startTime: Time when the test entity was started. 

710 :param setupDuration: Duration it took to set up the entity. 

711 :param testDuration: Duration of the entity's test run. 

712 :param teardownDuration: Duration it took to tear down the entity. 

713 :param totalDuration: Total duration of the entity's execution (setup + test + teardown) 

714 :param status: Status of the test case. 

715 :param assertionCount: Number of assertions within the test. 

716 :param failedAssertionCount: Number of failed assertions within the test. 

717 :param passedAssertionCount: Number of passed assertions within the test. 

718 :param warningCount: Count of encountered warnings. 

719 :param errorCount: Count of encountered errors. 

720 :param fatalCount: Count of encountered fatal errors. 

721 :param keyValuePairs: Mapping of key-value pairs to initialize the test case. 

722 :param parent: Reference to the parent test suite. 

723 :raises TypeError: If parameter 'parent' is not a Testsuite. 

724 :raises ValueError: If parameter 'assertionCount' is not consistent. 

725 """ 

726 

727 if parent is not None: 

728 if not isinstance(parent, Testsuite): 

729 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.") 

730 if version_info >= (3, 11): # pragma: no cover 

731 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

732 raise ex 

733 

734 parent._testcases[name] = self 

735 

736 super().__init__( 

737 name, 

738 startTime, 

739 setupDuration, testDuration, teardownDuration, totalDuration, 

740 warningCount, errorCount, fatalCount, 

741 expectedWarningCount, expectedErrorCount, expectedFatalCount, 

742 keyValuePairs, 

743 parent 

744 ) 

745 

746 if not isinstance(status, TestcaseStatus): 746 ↛ 747line 746 didn't jump to line 747 because the condition on line 746 was never true

747 ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.") 

748 if version_info >= (3, 11): # pragma: no cover 

749 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.") 

750 raise ex 

751 

752 self._status = status 

753 

754 if assertionCount is not None and not isinstance(assertionCount, int): 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true

755 ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.") 

756 if version_info >= (3, 11): # pragma: no cover 

757 ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.") 

758 raise ex 

759 

760 if failedAssertionCount is not None and not isinstance(failedAssertionCount, int): 760 ↛ 761line 760 didn't jump to line 761 because the condition on line 760 was never true

761 ex = TypeError(f"Parameter 'failedAssertionCount' is not of type 'int'.") 

762 if version_info >= (3, 11): # pragma: no cover 

763 ex.add_note(f"Got type '{getFullyQualifiedName(failedAssertionCount)}'.") 

764 raise ex 

765 

766 if passedAssertionCount is not None and not isinstance(passedAssertionCount, int): 766 ↛ 767line 766 didn't jump to line 767 because the condition on line 766 was never true

767 ex = TypeError(f"Parameter 'passedAssertionCount' is not of type 'int'.") 

768 if version_info >= (3, 11): # pragma: no cover 

769 ex.add_note(f"Got type '{getFullyQualifiedName(passedAssertionCount)}'.") 

770 raise ex 

771 

772 self._assertionCount = assertionCount 

773 if assertionCount is not None: 

774 if failedAssertionCount is not None: 

775 self._failedAssertionCount = failedAssertionCount 

776 

777 if passedAssertionCount is not None: 

778 if passedAssertionCount + failedAssertionCount != assertionCount: 

779 raise ValueError(f"passed assertion count ({passedAssertionCount}) + failed assertion count ({failedAssertionCount} != assertion count ({assertionCount}") 

780 

781 self._passedAssertionCount = passedAssertionCount 

782 else: 

783 self._passedAssertionCount = assertionCount - failedAssertionCount 

784 elif passedAssertionCount is not None: 

785 self._passedAssertionCount = passedAssertionCount 

786 self._failedAssertionCount = assertionCount - passedAssertionCount 

787 else: 

788 raise ValueError(f"Neither passed assertion count nor failed assertion count are provided.") 

789 elif failedAssertionCount is not None: 

790 self._failedAssertionCount = failedAssertionCount 

791 

792 if passedAssertionCount is not None: 

793 self._passedAssertionCount = passedAssertionCount 

794 self._assertionCount = passedAssertionCount + failedAssertionCount 

795 else: 

796 raise ValueError(f"Passed assertion count is mandatory, if failed assertion count is provided instead of assertion count.") 

797 elif passedAssertionCount is not None: 

798 raise ValueError(f"Assertion count or failed assertion count is mandatory, if passed assertion count is provided.") 

799 else: 

800 self._passedAssertionCount = None 

801 self._failedAssertionCount = None 

802 

803 @readonly 

804 def Status(self) -> TestcaseStatus: 

805 """ 

806 Read-only property to access the status of the test case. 

807 

808 :returns: The test case's status. 

809 """ 

810 return self._status 

811 

812 @readonly 

813 def AssertionCount(self) -> int: 

814 """ 

815 Read-only property to return the number of assertions (checks) in a test case. 

816 

817 :returns: Number of assertions. 

818 """ 

819 if self._assertionCount is None: 

820 return 0 

821 return self._assertionCount 

822 

823 @readonly 

824 def FailedAssertionCount(self) -> int: 

825 """ 

826 Read-only property to access the number of failed assertions (failed checks) in a test case. 

827 

828 :returns: Number of assertions. 

829 """ 

830 return self._failedAssertionCount 

831 

832 @readonly 

833 def PassedAssertionCount(self) -> int: 

834 """ 

835 Read-only property to access the number of passed assertions (successful checks) in a test case. 

836 

837 :returns: Number of passed assertions. 

838 """ 

839 return self._passedAssertionCount 

840 

841 def Copy(self) -> "Testcase": 

842 return self.__class__( 

843 self._name, 

844 self._startTime, 

845 self._setupDuration, 

846 self._testDuration, 

847 self._teardownDuration, 

848 self._totalDuration, 

849 self._status, 

850 self._warningCount, 

851 self._errorCount, 

852 self._fatalCount, 

853 self._expectedWarningCount, 

854 self._expectedErrorCount, 

855 self._expectedFatalCount, 

856 ) 

857 # TODO: copy key-value-pairs? 

858 

859 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType: 

860 if self._status is TestcaseStatus.Unknown: 860 ↛ 885line 860 didn't jump to line 885 because the condition on line 860 was always true

861 if self._assertionCount is None: 861 ↛ 862line 861 didn't jump to line 862 because the condition on line 861 was never true

862 self._status = TestcaseStatus.Passed 

863 elif self._assertionCount == 0: 863 ↛ 864line 863 didn't jump to line 864 because the condition on line 863 was never true

864 self._status = TestcaseStatus.Weak 

865 elif self._failedAssertionCount == 0: 

866 self._status = TestcaseStatus.Passed 

867 else: 

868 self._status = TestcaseStatus.Failed 

869 

870 if self._warningCount - self._expectedWarningCount > 0: 870 ↛ 871line 870 didn't jump to line 871 because the condition on line 870 was never true

871 self._status |= TestcaseStatus.Warned 

872 

873 if self._errorCount - self._expectedErrorCount > 0: 873 ↛ 874line 873 didn't jump to line 874 because the condition on line 873 was never true

874 self._status |= TestcaseStatus.Errored 

875 

876 if self._fatalCount - self._expectedFatalCount > 0: 876 ↛ 877line 876 didn't jump to line 877 because the condition on line 876 was never true

877 self._status |= TestcaseStatus.Aborted 

878 

879 if strict: 

880 self._status = self._status & ~TestcaseStatus.Passed | TestcaseStatus.Failed 

881 

882 # TODO: check for setup errors 

883 # TODO: check for teardown errors 

884 

885 totalDuration = timedelta() if self._totalDuration is None else self._totalDuration 

886 

887 return self._warningCount, self._errorCount, self._fatalCount, self._expectedWarningCount, self._expectedErrorCount, self._expectedFatalCount, totalDuration 

888 

889 def __str__(self) -> str: 

890 """ 

891 Formats the test case as human-readable incl. statistics. 

892 

893 :pycode:`f"<Testcase {}: {} - assert/pass/fail:{}/{}/{} - warn/error/fatal:{}/{}/{} - setup/test/teardown:{}/{}/{}>"` 

894 

895 :returns: Human-readable summary of a test case object. 

896 """ 

897 return ( 

898 f"<Testcase {self._name}: {self._status.name} -" 

899 f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -" 

900 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount} -" 

901 f" setup/test/teardown:{self._setupDuration:.3f}/{self._testDuration:.3f}/{self._teardownDuration:.3f}>" 

902 ) 

903 

904 

905@export 

906class TestsuiteBase(Base, Generic[TestsuiteType]): 

907 """ 

908 Base-class for all test suites and for test summaries. 

909 

910 A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root 

911 element in that hierarchy. While a test suite groups other test suites and test cases, a test summary can only group 

912 test suites. Thus, a test summary contains no test cases. 

913 """ 

914 

915 _kind: TestsuiteKind 

916 _status: TestsuiteStatus 

917 _testsuites: Dict[str, TestsuiteType] 

918 

919 _tests: int 

920 _inconsistent: int 

921 _excluded: int 

922 _skipped: int 

923 _errored: int 

924 _weak: int 

925 _failed: int 

926 _passed: int 

927 

928 def __init__( 

929 self, 

930 name: str, 

931 kind: TestsuiteKind = TestsuiteKind.Logical, 

932 startTime: Nullable[datetime] = None, 

933 setupDuration: Nullable[timedelta] = None, 

934 testDuration: Nullable[timedelta] = None, 

935 teardownDuration: Nullable[timedelta] = None, 

936 totalDuration: Nullable[timedelta] = None, 

937 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

938 warningCount: int = 0, 

939 errorCount: int = 0, 

940 fatalCount: int = 0, 

941 testsuites: Nullable[Iterable[TestsuiteType]] = None, 

942 keyValuePairs: Nullable[Mapping[str, Any]] = None, 

943 parent: Nullable["Testsuite"] = None 

944 ) -> None: 

945 """ 

946 Initializes the based-class fields of a test suite or test summary. 

947 

948 :param name: Name of the test entity. 

949 :param kind: Kind of the test entity. 

950 :param startTime: Time when the test entity was started. 

951 :param setupDuration: Duration it took to set up the entity. 

952 :param testDuration: Duration of all tests listed in the test entity. 

953 :param teardownDuration: Duration it took to tear down the entity. 

954 :param totalDuration: Total duration of the entity's execution (setup + test + teardown) 

955 :param status: Overall status of the test entity. 

956 :param warningCount: Count of encountered warnings incl. warnings from sub-elements. 

957 :param errorCount: Count of encountered errors incl. errors from sub-elements. 

958 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements. 

959 :param testsuites: List of test suites to initialize the test entity with. 

960 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with. 

961 :param parent: Reference to the parent test entity. 

962 :raises TypeError: If parameter 'parent' is not a TestsuiteBase. 

963 :raises TypeError: If parameter 'testsuites' is not iterable. 

964 :raises TypeError: If element in parameter 'testsuites' is not a Testsuite. 

965 :raises AlreadyInHierarchyException: If a test suite in parameter 'testsuites' is already part of a test entity hierarchy. 

966 :raises DuplicateTestsuiteException: If a test suite in parameter 'testsuites' is already listed (by name) in the list of test suites. 

967 """ 

968 if parent is not None: 

969 if not isinstance(parent, TestsuiteBase): 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true

970 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.") 

971 if version_info >= (3, 11): # pragma: no cover 

972 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

973 raise ex 

974 

975 parent._testsuites[name] = self 

976 

977 super().__init__( 

978 name, 

979 startTime, 

980 setupDuration, 

981 testDuration, 

982 teardownDuration, 

983 totalDuration, 

984 warningCount, 

985 errorCount, 

986 fatalCount, 

987 0, 0, 0, 

988 keyValuePairs, 

989 parent 

990 ) 

991 

992 self._kind = kind 

993 self._status = status 

994 

995 self._testsuites = {} 

996 if testsuites is not None: 

997 if not isinstance(testsuites, Iterable): 997 ↛ 998line 997 didn't jump to line 998 because the condition on line 997 was never true

998 ex = TypeError(f"Parameter 'testsuites' is not iterable.") 

999 if version_info >= (3, 11): # pragma: no cover 

1000 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.") 

1001 raise ex 

1002 

1003 for testsuite in testsuites: 

1004 if not isinstance(testsuite, Testsuite): 1004 ↛ 1005line 1004 didn't jump to line 1005 because the condition on line 1004 was never true

1005 ex = TypeError(f"Element of parameter 'testsuites' is not of type 'Testsuite'.") 

1006 if version_info >= (3, 11): # pragma: no cover 

1007 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.") 

1008 raise ex 

1009 

1010 if testsuite._parent is not None: 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true

1011 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") 

1012 

1013 if testsuite._name in self._testsuites: 

1014 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.") 

1015 

1016 testsuite._parent = self 

1017 self._testsuites[testsuite._name] = testsuite 

1018 

1019 self._status = TestsuiteStatus.Unknown 

1020 self._tests = 0 

1021 self._inconsistent = 0 

1022 self._excluded = 0 

1023 self._skipped = 0 

1024 self._errored = 0 

1025 self._weak = 0 

1026 self._failed = 0 

1027 self._passed = 0 

1028 

1029 @readonly 

1030 def Kind(self) -> TestsuiteKind: 

1031 """ 

1032 Read-only property to access the kind of the test suite. 

1033 

1034 Test suites are used to group test cases. This grouping can be due to language/framework specifics like tests 

1035 grouped by a module file or namespace. Others might be just logically grouped without any relation to a programming 

1036 language construct. 

1037 

1038 Test summaries always return kind ``Root``. 

1039 

1040 :returns: Kind of the test suite. 

1041 """ 

1042 return self._kind 

1043 

1044 @readonly 

1045 def Status(self) -> TestsuiteStatus: 

1046 """ 

1047 Read-only property to access the aggregated overall status of the test suite. 

1048 

1049 :returns: Overall status of the test suite. 

1050 """ 

1051 return self._status 

1052 

1053 @readonly 

1054 def Testsuites(self) -> Dict[str, TestsuiteType]: 

1055 """ 

1056 Read-only property to access a reference to the internal dictionary of test suites. 

1057 

1058 :returns: Reference to the dictionary of test suite. 

1059 """ 

1060 return self._testsuites 

1061 

1062 @readonly 

1063 def TestsuiteCount(self) -> int: 

1064 """ 

1065 Read-only property to return the number of all test suites in the test suite hierarchy. 

1066 

1067 :returns: Number of test suites. 

1068 """ 

1069 return 1 + sum(testsuite.TestsuiteCount for testsuite in self._testsuites.values()) 

1070 

1071 @readonly 

1072 def TestcaseCount(self) -> int: 

1073 """ 

1074 Read-only property to return the number of all test cases in the test entity hierarchy. 

1075 

1076 :returns: Number of test cases. 

1077 """ 

1078 return sum(testsuite.TestcaseCount for testsuite in self._testsuites.values()) 

1079 

1080 @readonly 

1081 def AssertionCount(self) -> int: 

1082 """ 

1083 Read-only property to return the number of all assertions in all test cases in the test entity hierarchy. 

1084 

1085 :returns: Number of assertions in all test cases. 

1086 """ 

1087 return sum(ts.AssertionCount for ts in self._testsuites.values()) 

1088 

1089 @readonly 

1090 def FailedAssertionCount(self) -> int: 

1091 """ 

1092 Read-only property to access the number of all failed assertions in all test cases in the test entity hierarchy. 

1093 

1094 :returns: Number of failed assertions in all test cases. 

1095 """ 

1096 raise NotImplementedError() 

1097 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount) 

1098 

1099 @readonly 

1100 def PassedAssertionCount(self) -> int: 

1101 """ 

1102 Read-only property to access the number of all passed assertions in all test cases in the test entity hierarchy. 

1103 

1104 :returns: Number of passed assertions in all test cases. 

1105 """ 

1106 raise NotImplementedError() 

1107 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount) 

1108 

1109 @readonly 

1110 def Tests(self) -> int: 

1111 """ 

1112 Read-only property to access the number of tests in this entity. 

1113 

1114 :returns: Number of tests. 

1115 """ 

1116 return self._tests 

1117 

1118 @readonly 

1119 def Inconsistent(self) -> int: 

1120 """ 

1121 Read-only property to access the number of inconsistent tests in the test suite hierarchy. 

1122 

1123 :returns: Number of inconsistent tests. 

1124 """ 

1125 return self._inconsistent 

1126 

1127 @readonly 

1128 def Excluded(self) -> int: 

1129 """ 

1130 Read-only property to access the number of excluded tests in the test suite hierarchy. 

1131 

1132 :returns: Number of excluded tests. 

1133 """ 

1134 return self._excluded 

1135 

1136 @readonly 

1137 def Skipped(self) -> int: 

1138 """ 

1139 Read-only property to access the number of skipped tests in the test suite hierarchy. 

1140 

1141 :returns: Number of skipped tests. 

1142 """ 

1143 return self._skipped 

1144 

1145 @readonly 

1146 def Errored(self) -> int: 

1147 """ 

1148 Read-only property to access the number of tests with errors in the test suite hierarchy. 

1149 

1150 :returns: Number of errored tests. 

1151 """ 

1152 return self._errored 

1153 

1154 @readonly 

1155 def Weak(self) -> int: 

1156 """ 

1157 Read-only property to access the number of weak tests in the test suite hierarchy. 

1158 

1159 :returns: Number of weak tests. 

1160 """ 

1161 return self._weak 

1162 

1163 @readonly 

1164 def Failed(self) -> int: 

1165 """ 

1166 Read-only property to access the number of failed tests in the test suite hierarchy. 

1167 

1168 :returns: Number of failed tests. 

1169 """ 

1170 return self._failed 

1171 

1172 @readonly 

1173 def Passed(self) -> int: 

1174 """ 

1175 Read-only property to access the number of passed tests in the test suite hierarchy. 

1176 

1177 :returns: Number of passed tests. 

1178 """ 

1179 return self._passed 

1180 

1181 @readonly 

1182 def WarningCount(self) -> int: 

1183 """ 

1184 Read-only property to access the number of warnings in this entity. 

1185 

1186 :returns: Number of warnings. 

1187 """ 

1188 raise NotImplementedError() 

1189 # return self._warningCount 

1190 

1191 @readonly 

1192 def ErrorCount(self) -> int: 

1193 """ 

1194 Read-only property to access the number of errors in this entity. 

1195 

1196 :returns: Number of errors. 

1197 """ 

1198 raise NotImplementedError() 

1199 # return self._errorCount 

1200 

1201 @readonly 

1202 def FatalCount(self) -> int: 

1203 """ 

1204 Read-only property to access the number of fatal errors in this entity. 

1205 

1206 :returns: Number of fatal errors. 

1207 """ 

1208 raise NotImplementedError() 

1209 # return self._fatalCount 

1210 

1211 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: 

1212 tests = 0 

1213 inconsistent = 0 

1214 excluded = 0 

1215 skipped = 0 

1216 errored = 0 

1217 weak = 0 

1218 failed = 0 

1219 passed = 0 

1220 

1221 warningCount = 0 

1222 errorCount = 0 

1223 fatalCount = 0 

1224 

1225 expectedWarningCount = 0 

1226 expectedErrorCount = 0 

1227 expectedFatalCount = 0 

1228 

1229 totalDuration = timedelta() 

1230 

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

1232 t, i, ex, s, e, w, f, p, wc, ec, fc, ewc, eec, efc, td = testsuite.Aggregate(strict) 

1233 tests += t 

1234 inconsistent += i 

1235 excluded += ex 

1236 skipped += s 

1237 errored += e 

1238 weak += w 

1239 failed += f 

1240 passed += p 

1241 

1242 warningCount += wc 

1243 errorCount += ec 

1244 fatalCount += fc 

1245 

1246 expectedWarningCount += ewc 

1247 expectedErrorCount += eec 

1248 expectedFatalCount += efc 

1249 

1250 totalDuration += td 

1251 

1252 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration 

1253 

1254 def AddTestsuite(self, testsuite: TestsuiteType) -> None: 

1255 """ 

1256 Add a test suite to the list of test suites. 

1257 

1258 :param testsuite: The test suite to add. 

1259 :raises ValueError: If parameter 'testsuite' is None. 

1260 :raises TypeError: If parameter 'testsuite' is not a Testsuite. 

1261 :raises AlreadyInHierarchyException: If parameter 'testsuite' is already part of a test entity hierarchy. 

1262 :raises DuplicateTestcaseException: If parameter 'testsuite' is already listed (by name) in the list of test suites. 

1263 """ 

1264 if testsuite is None: 1264 ↛ 1265line 1264 didn't jump to line 1265 because the condition on line 1264 was never true

1265 raise ValueError("Parameter 'testsuite' is None.") 

1266 elif not isinstance(testsuite, Testsuite): 1266 ↛ 1267line 1266 didn't jump to line 1267 because the condition on line 1266 was never true

1267 ex = TypeError(f"Parameter 'testsuite' is not of type 'Testsuite'.") 

1268 if version_info >= (3, 11): # pragma: no cover 

1269 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.") 

1270 raise ex 

1271 

1272 if testsuite._parent is not None: 1272 ↛ 1273line 1272 didn't jump to line 1273 because the condition on line 1272 was never true

1273 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") 

1274 

1275 if testsuite._name in self._testsuites: 

1276 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.") 

1277 

1278 testsuite._parent = self 

1279 self._testsuites[testsuite._name] = testsuite 

1280 

1281 def AddTestsuites(self, testsuites: Iterable[TestsuiteType]) -> None: 

1282 """ 

1283 Add a list of test suites to the list of test suites. 

1284 

1285 :param testsuites: List of test suites to add. 

1286 :raises ValueError: If parameter 'testsuites' is None. 

1287 :raises TypeError: If parameter 'testsuites' is not iterable. 

1288 """ 

1289 if testsuites is None: 1289 ↛ 1290line 1289 didn't jump to line 1290 because the condition on line 1289 was never true

1290 raise ValueError("Parameter 'testsuites' is None.") 

1291 elif not isinstance(testsuites, Iterable): 1291 ↛ 1292line 1291 didn't jump to line 1292 because the condition on line 1291 was never true

1292 ex = TypeError(f"Parameter 'testsuites' is not iterable.") 

1293 if version_info >= (3, 11): # pragma: no cover 

1294 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.") 

1295 raise ex 

1296 

1297 for testsuite in testsuites: 

1298 self.AddTestsuite(testsuite) 

1299 

1300 @abstractmethod 

1301 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: 

1302 pass 

1303 

1304 def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]: 

1305 return self.Iterate(scheme) 

1306 

1307 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]: 

1308 return self.Iterate(scheme) 

1309 

1310 def ToTree(self) -> Node: 

1311 rootNode = Node(value=self._name) 

1312 

1313 def convertTestcase(testcase: Testcase, parentNode: Node) -> None: 

1314 _ = Node(value=testcase._name, parent=parentNode) 

1315 

1316 def convertTestsuite(testsuite: Testsuite, parentNode: Node) -> None: 

1317 testsuiteNode = Node(value=testsuite._name, parent=parentNode) 

1318 

1319 for ts in testsuite._testsuites.values(): 

1320 convertTestsuite(ts, testsuiteNode) 

1321 

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

1323 convertTestcase(tc, testsuiteNode) 

1324 

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

1326 convertTestsuite(testsuite, rootNode) 

1327 

1328 return rootNode 

1329 

1330 

1331@export 

1332class Testsuite(TestsuiteBase[TestsuiteType]): 

1333 """ 

1334 A testsuite is a mid-level element in the test entity hierarchy representing a group of tests. 

1335 

1336 Test suites contain test cases and optionally other test suites. Test suites can be grouped by test suites to form a 

1337 hierarchy of test entities. The root of the hierarchy is a test summary. 

1338 """ 

1339 

1340 _testcases: Dict[str, "Testcase"] 

1341 

1342 def __init__( 

1343 self, 

1344 name: str, 

1345 kind: TestsuiteKind = TestsuiteKind.Logical, 

1346 startTime: Nullable[datetime] = None, 

1347 setupDuration: Nullable[timedelta] = None, 

1348 testDuration: Nullable[timedelta] = None, 

1349 teardownDuration: Nullable[timedelta] = None, 

1350 totalDuration: Nullable[timedelta] = None, 

1351 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1352 warningCount: int = 0, 

1353 errorCount: int = 0, 

1354 fatalCount: int = 0, 

1355 testsuites: Nullable[Iterable[TestsuiteType]] = None, 

1356 testcases: Nullable[Iterable["Testcase"]] = None, 

1357 keyValuePairs: Nullable[Mapping[str, Any]] = None, 

1358 parent: Nullable[TestsuiteType] = None 

1359 ) -> None: 

1360 """ 

1361 Initializes the fields of a test suite. 

1362 

1363 :param name: Name of the test suite. 

1364 :param kind: Kind of the test suite. 

1365 :param startTime: Time when the test suite was started. 

1366 :param setupDuration: Duration it took to set up the test suite. 

1367 :param testDuration: Duration of all tests listed in the test suite. 

1368 :param teardownDuration: Duration it took to tear down the test suite. 

1369 :param totalDuration: Total duration of the entity's execution (setup + test + teardown) 

1370 :param status: Overall status of the test suite. 

1371 :param warningCount: Count of encountered warnings incl. warnings from sub-elements. 

1372 :param errorCount: Count of encountered errors incl. errors from sub-elements. 

1373 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements. 

1374 :param testsuites: List of test suites to initialize the test suite with. 

1375 :param testcases: List of test cases to initialize the test suite with. 

1376 :param keyValuePairs: Mapping of key-value pairs to initialize the test suite with. 

1377 :param parent: Reference to the parent test entity. 

1378 :raises TypeError: If parameter 'testcases' is not iterable. 

1379 :raises TypeError: If element in parameter 'testcases' is not a Testcase. 

1380 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy. 

1381 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases. 

1382 """ 

1383 super().__init__( 

1384 name, 

1385 kind, 

1386 startTime, 

1387 setupDuration, 

1388 testDuration, 

1389 teardownDuration, 

1390 totalDuration, 

1391 status, 

1392 warningCount, 

1393 errorCount, 

1394 fatalCount, 

1395 testsuites, 

1396 keyValuePairs, 

1397 parent 

1398 ) 

1399 

1400 # self._testDuration = testDuration 

1401 

1402 self._testcases = {} 

1403 if testcases is not None: 

1404 if not isinstance(testcases, Iterable): 1404 ↛ 1405line 1404 didn't jump to line 1405 because the condition on line 1404 was never true

1405 ex = TypeError(f"Parameter 'testcases' is not iterable.") 

1406 if version_info >= (3, 11): # pragma: no cover 

1407 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.") 

1408 raise ex 

1409 

1410 for testcase in testcases: 

1411 if not isinstance(testcase, Testcase): 1411 ↛ 1412line 1411 didn't jump to line 1412 because the condition on line 1411 was never true

1412 ex = TypeError(f"Element of parameter 'testcases' is not of type 'Testcase'.") 

1413 if version_info >= (3, 11): # pragma: no cover 

1414 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.") 

1415 raise ex 

1416 

1417 if testcase._parent is not None: 1417 ↛ 1418line 1417 didn't jump to line 1418 because the condition on line 1417 was never true

1418 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") 

1419 

1420 if testcase._name in self._testcases: 

1421 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.") 

1422 

1423 testcase._parent = self 

1424 self._testcases[testcase._name] = testcase 

1425 

1426 @readonly 

1427 def Testcases(self) -> Dict[str, "Testcase"]: 

1428 """ 

1429 Read-only property to access a reference to the internal dictionary of test cases. 

1430 

1431 :returns: Reference to the dictionary of test cases. 

1432 """ 

1433 return self._testcases 

1434 

1435 @readonly 

1436 def TestcaseCount(self) -> int: 

1437 """ 

1438 Read-only property to return the number of all test cases in the test entity hierarchy. 

1439 

1440 :returns: Number of test cases. 

1441 """ 

1442 return super().TestcaseCount + len(self._testcases) 

1443 

1444 @readonly 

1445 def AssertionCount(self) -> int: 

1446 """ 

1447 Read-only property to return the number of assertions in this testsuite and its testcases. 

1448 

1449 :returns: Sum of the inherited assertion count and the testcases' assertion counts. 

1450 """ 

1451 return super().AssertionCount + sum(tc.AssertionCount for tc in self._testcases.values()) 

1452 

1453 def Copy(self) -> "Testsuite": 

1454 return self.__class__( 

1455 self._name, 

1456 self._startTime, 

1457 self._setupDuration, 

1458 self._teardownDuration, 

1459 self._totalDuration, 

1460 self._status, 

1461 self._warningCount, 

1462 self._errorCount, 

1463 self._fatalCount 

1464 ) 

1465 

1466 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: 

1467 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration = super().Aggregate() 

1468 

1469 for testcase in self._testcases.values(): 

1470 wc, ec, fc, ewc, eec, efc, td = testcase.Aggregate(strict) 

1471 

1472 tests += 1 

1473 

1474 warningCount += wc 

1475 errorCount += ec 

1476 fatalCount += fc 

1477 

1478 expectedWarningCount += ewc 

1479 expectedErrorCount += eec 

1480 expectedFatalCount += efc 

1481 

1482 totalDuration += td 

1483 

1484 status = testcase._status 

1485 if status is TestcaseStatus.Unknown: 1485 ↛ 1486line 1485 didn't jump to line 1486 because the condition on line 1485 was never true

1486 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.") 

1487 elif TestcaseStatus.Inconsistent in status: 1487 ↛ 1488line 1487 didn't jump to line 1488 because the condition on line 1487 was never true

1488 inconsistent += 1 

1489 elif status is TestcaseStatus.Excluded: 1489 ↛ 1490line 1489 didn't jump to line 1490 because the condition on line 1489 was never true

1490 excluded += 1 

1491 elif status is TestcaseStatus.Skipped: 

1492 skipped += 1 

1493 elif status is TestcaseStatus.Errored: 1493 ↛ 1494line 1493 didn't jump to line 1494 because the condition on line 1493 was never true

1494 errored += 1 

1495 elif status is TestcaseStatus.Weak: 1495 ↛ 1496line 1495 didn't jump to line 1496 because the condition on line 1495 was never true

1496 weak += 1 

1497 elif status is TestcaseStatus.Passed: 

1498 passed += 1 

1499 elif status is TestcaseStatus.Failed: 1499 ↛ 1501line 1499 didn't jump to line 1501 because the condition on line 1499 was always true

1500 failed += 1 

1501 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown: 

1502 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.") 

1503 else: 

1504 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.") 

1505 

1506 self._tests = tests 

1507 self._inconsistent = inconsistent 

1508 self._excluded = excluded 

1509 self._skipped = skipped 

1510 self._errored = errored 

1511 self._weak = weak 

1512 self._failed = failed 

1513 self._passed = passed 

1514 

1515 self._warningCount = warningCount 

1516 self._errorCount = errorCount 

1517 self._fatalCount = fatalCount 

1518 

1519 self._expectedWarningCount = expectedWarningCount 

1520 self._expectedErrorCount = expectedErrorCount 

1521 self._expectedFatalCount = expectedFatalCount 

1522 

1523 if self._totalDuration is None: 

1524 self._totalDuration = totalDuration 

1525 

1526 if errored > 0: 1526 ↛ 1527line 1526 didn't jump to line 1527 because the condition on line 1526 was never true

1527 self._status = TestsuiteStatus.Errored 

1528 elif failed > 0: 

1529 self._status = TestsuiteStatus.Failed 

1530 elif tests == 0: 1530 ↛ 1531line 1530 didn't jump to line 1531 because the condition on line 1530 was never true

1531 self._status = TestsuiteStatus.Empty 

1532 elif tests - skipped == passed: 1532 ↛ 1534line 1532 didn't jump to line 1534 because the condition on line 1532 was always true

1533 self._status = TestsuiteStatus.Passed 

1534 elif tests == skipped: 

1535 self._status = TestsuiteStatus.Skipped 

1536 else: 

1537 self._status = TestsuiteStatus.Unknown 

1538 

1539 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration 

1540 

1541 def AddTestcase(self, testcase: "Testcase") -> None: 

1542 """ 

1543 Add a test case to the list of test cases. 

1544 

1545 :param testcase: The test case to add. 

1546 :raises ValueError: If parameter 'testcase' is None. 

1547 :raises TypeError: If parameter 'testcase' is not a Testcase. 

1548 :raises AlreadyInHierarchyException: If parameter 'testcase' is already part of a test entity hierarchy. 

1549 :raises DuplicateTestcaseException: If parameter 'testcase' is already listed (by name) in the list of test cases. 

1550 """ 

1551 if testcase is None: 1551 ↛ 1552line 1551 didn't jump to line 1552 because the condition on line 1551 was never true

1552 raise ValueError("Parameter 'testcase' is None.") 

1553 elif not isinstance(testcase, Testcase): 1553 ↛ 1554line 1553 didn't jump to line 1554 because the condition on line 1553 was never true

1554 ex = TypeError(f"Parameter 'testcase' is not of type 'Testcase'.") 

1555 if version_info >= (3, 11): # pragma: no cover 

1556 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.") 

1557 raise ex 

1558 

1559 if testcase._parent is not None: 1559 ↛ 1560line 1559 didn't jump to line 1560 because the condition on line 1559 was never true

1560 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") 

1561 

1562 if testcase._name in self._testcases: 

1563 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.") 

1564 

1565 testcase._parent = self 

1566 self._testcases[testcase._name] = testcase 

1567 

1568 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None: 

1569 """ 

1570 Add a list of test cases to the list of test cases. 

1571 

1572 :param testcases: List of test cases to add. 

1573 :raises ValueError: If parameter 'testcases' is None. 

1574 :raises TypeError: If parameter 'testcases' is not iterable. 

1575 """ 

1576 if testcases is None: 1576 ↛ 1577line 1576 didn't jump to line 1577 because the condition on line 1576 was never true

1577 raise ValueError("Parameter 'testcases' is None.") 

1578 elif not isinstance(testcases, Iterable): 1578 ↛ 1579line 1578 didn't jump to line 1579 because the condition on line 1578 was never true

1579 ex = TypeError(f"Parameter 'testcases' is not iterable.") 

1580 if version_info >= (3, 11): # pragma: no cover 

1581 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.") 

1582 raise ex 

1583 

1584 for testcase in testcases: 

1585 self.AddTestcase(testcase) 

1586 

1587 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: 

1588 if IterationScheme.PreOrder in scheme: 

1589 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: 

1590 yield self 

1591 

1592 if IterationScheme.IncludeTestcases in scheme: 

1593 for testcase in self._testcases.values(): 

1594 yield testcase 

1595 

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

1597 yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf) 

1598 

1599 if IterationScheme.PostOrder in scheme: 

1600 if IterationScheme.IncludeTestcases in scheme: 

1601 for testcase in self._testcases.values(): 

1602 yield testcase 

1603 

1604 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: 

1605 yield self 

1606 

1607 def __str__(self) -> str: 

1608 return ( 

1609 f"<Testsuite {self._name}: {self._status.name} -" 

1610 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -" 

1611 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>" 

1612 ) 

1613 

1614 

1615@export 

1616class TestsuiteSummary(TestsuiteBase[TestsuiteType]): 

1617 """ 

1618 A testsuite summary is the root element in the test entity hierarchy representing a summary of all test suites and cases. 

1619 

1620 The testsuite summary contains test suites, which in turn can contain test suites and test cases. 

1621 """ 

1622 

1623 def __init__( 

1624 self, 

1625 name: str, 

1626 startTime: Nullable[datetime] = None, 

1627 setupDuration: Nullable[timedelta] = None, 

1628 testDuration: Nullable[timedelta] = None, 

1629 teardownDuration: Nullable[timedelta] = None, 

1630 totalDuration: Nullable[timedelta] = None, 

1631 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1632 warningCount: int = 0, 

1633 errorCount: int = 0, 

1634 fatalCount: int = 0, 

1635 testsuites: Nullable[Iterable[TestsuiteType]] = None, 

1636 keyValuePairs: Nullable[Mapping[str, Any]] = None, 

1637 parent: Nullable[TestsuiteType] = None 

1638 ) -> None: 

1639 """ 

1640 Initializes the fields of a test summary. 

1641 

1642 :param name: Name of the test summary. 

1643 :param startTime: Time when the test summary was started. 

1644 :param setupDuration: Duration it took to set up the test summary. 

1645 :param testDuration: Duration of all tests listed in the test summary. 

1646 :param teardownDuration: Duration it took to tear down the test summary. 

1647 :param totalDuration: Total duration of the entity's execution (setup + test + teardown) 

1648 :param status: Overall status of the test summary. 

1649 :param warningCount: Count of encountered warnings incl. warnings from sub-elements. 

1650 :param errorCount: Count of encountered errors incl. errors from sub-elements. 

1651 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements. 

1652 :param testsuites: List of test suites to initialize the test summary with. 

1653 :param keyValuePairs: Mapping of key-value pairs to initialize the test summary with. 

1654 :param parent: Reference to the parent test summary. 

1655 """ 

1656 super().__init__( 

1657 name, 

1658 TestsuiteKind.Root, 

1659 startTime, setupDuration, testDuration, teardownDuration, totalDuration, 

1660 status, 

1661 warningCount, errorCount, fatalCount, 

1662 testsuites, 

1663 keyValuePairs, 

1664 parent 

1665 ) 

1666 

1667 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: 

1668 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration = super().Aggregate(strict) 

1669 

1670 self._tests = tests 

1671 self._inconsistent = inconsistent 

1672 self._excluded = excluded 

1673 self._skipped = skipped 

1674 self._errored = errored 

1675 self._weak = weak 

1676 self._failed = failed 

1677 self._passed = passed 

1678 

1679 self._warningCount = warningCount 

1680 self._errorCount = errorCount 

1681 self._fatalCount = fatalCount 

1682 

1683 self._expectedWarningCount = expectedWarningCount 

1684 self._expectedErrorCount = expectedErrorCount 

1685 self._expectedFatalCount = expectedFatalCount 

1686 

1687 if self._totalDuration is None: 1687 ↛ 1690line 1687 didn't jump to line 1690 because the condition on line 1687 was always true

1688 self._totalDuration = totalDuration 

1689 

1690 if errored > 0: 1690 ↛ 1691line 1690 didn't jump to line 1691 because the condition on line 1690 was never true

1691 self._status = TestsuiteStatus.Errored 

1692 elif failed > 0: 

1693 self._status = TestsuiteStatus.Failed 

1694 elif tests == 0: 1694 ↛ 1695line 1694 didn't jump to line 1695 because the condition on line 1694 was never true

1695 self._status = TestsuiteStatus.Empty 

1696 elif tests - skipped == passed: 1696 ↛ 1698line 1696 didn't jump to line 1698 because the condition on line 1696 was always true

1697 self._status = TestsuiteStatus.Passed 

1698 elif tests == skipped: 

1699 self._status = TestsuiteStatus.Skipped 

1700 elif tests == excluded: 

1701 self._status = TestsuiteStatus.Excluded 

1702 else: 

1703 self._status = TestsuiteStatus.Unknown 

1704 

1705 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration 

1706 

1707 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: 

1708 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme: 

1709 yield self 

1710 

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

1712 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf) 

1713 

1714 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme: 1714 ↛ 1715line 1714 didn't jump to line 1715 because the condition on line 1714 was never true

1715 yield self 

1716 

1717 def __str__(self) -> str: 

1718 return ( 

1719 f"<TestsuiteSummary {self._name}: {self._status.name} -" 

1720 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -" 

1721 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>" 

1722 ) 

1723 

1724 

1725@export 

1726class Document(metaclass=ExtendedType, mixin=True): 

1727 """A mixin-class representing a unit test summary document (file).""" 

1728 

1729 _path: Path #: Path to the YAML file. 

1730 

1731 _analysisDuration: float #: TODO: replace by Timer; should be timedelta? 

1732 _modelConversion: float #: TODO: replace by Timer; should be timedelta? 

1733 

1734 def __init__(self, reportFile: Path, analyzeAndConvert: bool = False) -> None: 

1735 self._path = reportFile 

1736 

1737 self._analysisDuration = -1.0 

1738 self._modelConversion = -1.0 

1739 

1740 if analyzeAndConvert: 

1741 self.Analyze() 

1742 self.Convert() 

1743 

1744 @readonly 

1745 def Path(self) -> Path: 

1746 """ 

1747 Read-only property to access the path to the file of this document. 

1748 

1749 :returns: The document's path to the file. 

1750 """ 

1751 return self._path 

1752 

1753 @readonly 

1754 def AnalysisDuration(self) -> timedelta: 

1755 """ 

1756 Read-only property to return analysis duration. 

1757 

1758 .. note:: 

1759 

1760 This includes usually the duration to validate and parse the file format, but it excludes the time to convert the 

1761 content to the test entity hierarchy. 

1762 

1763 :returns: Duration to analyze the document. 

1764 """ 

1765 return timedelta(seconds=self._analysisDuration) 

1766 

1767 @readonly 

1768 def ModelConversionDuration(self) -> timedelta: 

1769 """ 

1770 Read-only property to return conversion duration. 

1771 

1772 .. note:: 

1773 

1774 This includes usually the duration to convert the document's content to the test entity hierarchy. It might also 

1775 include the duration to (re-)aggregate all states and statistics in the hierarchy. 

1776 

1777 :returns: Duration to convert the document. 

1778 """ 

1779 return timedelta(seconds=self._modelConversion) 

1780 

1781 @abstractmethod 

1782 def Analyze(self) -> None: 

1783 """Analyze and validate the document's content.""" 

1784 

1785 # @abstractmethod 

1786 # def Write(self, path: Nullable[Path] = None, overwrite: bool = False): 

1787 # pass 

1788 

1789 @abstractmethod 

1790 def Convert(self): 

1791 """Convert the document's content to an instance of the test entity hierarchy.""" 

1792 

1793 

1794@export 

1795class Merged(metaclass=ExtendedType, mixin=True): 

1796 """A mixin-class representing a merged test entity.""" 

1797 

1798 _mergedCount: int 

1799 

1800 def __init__(self, mergedCount: int = 1) -> None: 

1801 self._mergedCount = mergedCount 

1802 

1803 @readonly 

1804 def MergedCount(self) -> int: 

1805 """ 

1806 Read-only property to access how many source entities were merged into this one. 

1807 

1808 :returns: Number of merged entities. 

1809 """ 

1810 return self._mergedCount 

1811 

1812 

1813@export 

1814class Combined(metaclass=ExtendedType, mixin=True): 

1815 _combinedCount: int 

1816 

1817 def __init__(self, combinedCound: int = 1) -> None: 

1818 self._combinedCount = combinedCound 

1819 

1820 @readonly 

1821 def CombinedCount(self) -> int: 

1822 """ 

1823 Read-only property to access how many source entities were combined into this one. 

1824 

1825 :returns: Number of combined entities. 

1826 """ 

1827 return self._combinedCount 

1828 

1829 

1830@export 

1831class MergedTestcase(Testcase, Merged): 

1832 _mergedTestcases: List[Testcase] 

1833 

1834 def __init__( 

1835 self, 

1836 testcase: Testcase, 

1837 parent: Nullable["Testsuite"] = None 

1838 ) -> None: 

1839 if testcase is None: 1839 ↛ 1840line 1839 didn't jump to line 1840 because the condition on line 1839 was never true

1840 raise ValueError(f"Parameter 'testcase' is None.") 

1841 

1842 super().__init__( 

1843 testcase._name, 

1844 testcase._startTime, 

1845 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration, 

1846 TestcaseStatus.Unknown, 

1847 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount, 

1848 testcase._warningCount, testcase._errorCount, testcase._fatalCount, 

1849 testcase._expectedWarningCount, testcase._expectedErrorCount, testcase._expectedFatalCount, 

1850 parent 

1851 ) 

1852 Merged.__init__(self) 

1853 

1854 self._mergedTestcases = [testcase] 

1855 

1856 @readonly 

1857 def Status(self) -> TestcaseStatus: 

1858 """ 

1859 Read-only property to access the status merged from all source testcases. 

1860 

1861 :returns: Merged status. ``TestcaseStatus.Inconsistent`` if the sources disagree. 

1862 """ 

1863 if self._status is TestcaseStatus.Unknown: 1863 ↛ 1870line 1863 didn't jump to line 1870 because the condition on line 1863 was always true

1864 status = self._mergedTestcases[0]._status 

1865 for mtc in self._mergedTestcases[1:]: 

1866 status @= mtc._status 

1867 

1868 self._status = status 

1869 

1870 return self._status 

1871 

1872 @readonly 

1873 def SummedAssertionCount(self) -> int: 

1874 """ 

1875 Read-only property to return the number of assertions across all merged testcases. 

1876 

1877 :returns: Sum of the merged testcases' assertion counts. 

1878 """ 

1879 return sum(tc._assertionCount for tc in self._mergedTestcases) 

1880 

1881 @readonly 

1882 def SummedPassedAssertionCount(self) -> int: 

1883 """ 

1884 Read-only property to return the number of passed assertions across all merged testcases. 

1885 

1886 :returns: Sum of the merged testcases' passed assertion counts. 

1887 """ 

1888 return sum(tc._passedAssertionCount for tc in self._mergedTestcases) 

1889 

1890 @readonly 

1891 def SummedFailedAssertionCount(self) -> int: 

1892 """ 

1893 Read-only property to return the number of failed assertions across all merged testcases. 

1894 

1895 :returns: Sum of the merged testcases' failed assertion counts. 

1896 """ 

1897 return sum(tc._failedAssertionCount for tc in self._mergedTestcases) 

1898 

1899 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType: 

1900 firstMTC = self._mergedTestcases[0] 

1901 

1902 status = firstMTC._status 

1903 warningCount = firstMTC._warningCount 

1904 errorCount = firstMTC._errorCount 

1905 fatalCount = firstMTC._fatalCount 

1906 totalDuration = firstMTC._totalDuration 

1907 

1908 for mtc in self._mergedTestcases[1:]: 

1909 status @= mtc._status 

1910 warningCount += mtc._warningCount 

1911 errorCount += mtc._errorCount 

1912 fatalCount += mtc._fatalCount 

1913 

1914 self._status = status 

1915 

1916 return warningCount, errorCount, fatalCount, self._expectedWarningCount, self._expectedErrorCount, self._expectedFatalCount, totalDuration 

1917 

1918 def Merge(self, tc: Testcase) -> None: 

1919 self._mergedCount += 1 

1920 

1921 self._mergedTestcases.append(tc) 

1922 

1923 self._warningCount += tc._warningCount 

1924 self._errorCount += tc._errorCount 

1925 self._fatalCount += tc._fatalCount 

1926 

1927 def ToTestcase(self) -> Testcase: 

1928 return Testcase( 

1929 self._name, 

1930 self._startTime, 

1931 self._setupDuration, 

1932 self._testDuration, 

1933 self._teardownDuration, 

1934 self._totalDuration, 

1935 self._status, 

1936 self._assertionCount, 

1937 self._failedAssertionCount, 

1938 self._passedAssertionCount, 

1939 self._warningCount, 

1940 self._errorCount, 

1941 self._fatalCount 

1942 ) 

1943 

1944 

1945@export 

1946class MergedTestsuite(Testsuite, Merged): 

1947 def __init__( 

1948 self, 

1949 testsuite: Testsuite, 

1950 addTestsuites: bool = False, 

1951 addTestcases: bool = False, 

1952 parent: Nullable["Testsuite"] = None 

1953 ) -> None: 

1954 if testsuite is None: 1954 ↛ 1955line 1954 didn't jump to line 1955 because the condition on line 1954 was never true

1955 raise ValueError(f"Parameter 'testsuite' is None.") 

1956 

1957 super().__init__( 

1958 testsuite._name, 

1959 testsuite._kind, 

1960 testsuite._startTime, 

1961 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration, 

1962 TestsuiteStatus.Unknown, 

1963 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount, 

1964 parent 

1965 ) 

1966 Merged.__init__(self) 

1967 

1968 if addTestsuites: 1968 ↛ 1973line 1968 didn't jump to line 1973 because the condition on line 1968 was always true

1969 for ts in testsuite._testsuites.values(): 

1970 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases) 

1971 self.AddTestsuite(mergedTestsuite) 

1972 

1973 if addTestcases: 1973 ↛ exitline 1973 didn't return from function '__init__' because the condition on line 1973 was always true

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

1975 mergedTestcase = MergedTestcase(tc) 

1976 self.AddTestcase(mergedTestcase) 

1977 

1978 def Merge(self, testsuite: Testsuite) -> None: 

1979 self._mergedCount += 1 

1980 

1981 for ts in testsuite._testsuites.values(): 

1982 if ts._name in self._testsuites: 1982 ↛ 1985line 1982 didn't jump to line 1985 because the condition on line 1982 was always true

1983 self._testsuites[ts._name].Merge(ts) 

1984 else: 

1985 mergedTestsuite = MergedTestsuite(ts, addTestsuites=True, addTestcases=True) 

1986 self.AddTestsuite(mergedTestsuite) 

1987 

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

1989 if tc._name in self._testcases: 1989 ↛ 1992line 1989 didn't jump to line 1992 because the condition on line 1989 was always true

1990 self._testcases[tc._name].Merge(tc) 

1991 else: 

1992 mergedTestcase = MergedTestcase(tc) 

1993 self.AddTestcase(mergedTestcase) 

1994 

1995 def ToTestsuite(self) -> Testsuite: 

1996 testsuite = Testsuite( 

1997 self._name, 

1998 self._kind, 

1999 self._startTime, 

2000 self._setupDuration, 

2001 self._testDuration, 

2002 self._teardownDuration, 

2003 self._totalDuration, 

2004 self._status, 

2005 self._warningCount, 

2006 self._errorCount, 

2007 self._fatalCount, 

2008 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values()), 

2009 testcases=(tc.ToTestcase() for tc in self._testcases.values()) 

2010 ) 

2011 

2012 testsuite._tests = self._tests 

2013 testsuite._excluded = self._excluded 

2014 testsuite._inconsistent = self._inconsistent 

2015 testsuite._skipped = self._skipped 

2016 testsuite._errored = self._errored 

2017 testsuite._weak = self._weak 

2018 testsuite._failed = self._failed 

2019 testsuite._passed = self._passed 

2020 

2021 return testsuite 

2022 

2023 

2024@export 

2025class MergedTestsuiteSummary(TestsuiteSummary, Merged): 

2026 _mergedFiles: Dict[Path, TestsuiteSummary] 

2027 

2028 def __init__(self, name: str) -> None: 

2029 super().__init__(name) 

2030 Merged.__init__(self, mergedCount=0) 

2031 

2032 self._mergedFiles = {} 

2033 

2034 def Merge(self, testsuiteSummary: TestsuiteSummary) -> None: 

2035 # if summary.File in self._mergedFiles: 

2036 # raise 

2037 

2038 # FIXME: a summary is not necessarily a file 

2039 self._mergedCount += 1 

2040 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary 

2041 

2042 for testsuite in testsuiteSummary._testsuites.values(): 

2043 if testsuite._name in self._testsuites: 

2044 self._testsuites[testsuite._name].Merge(testsuite) 

2045 else: 

2046 mergedTestsuite = MergedTestsuite(testsuite, addTestsuites=True, addTestcases=True) 

2047 self.AddTestsuite(mergedTestsuite) 

2048 

2049 def ToTestsuiteSummary(self) -> TestsuiteSummary: 

2050 testsuiteSummary = TestsuiteSummary( 

2051 self._name, 

2052 self._startTime, 

2053 self._setupDuration, 

2054 self._testDuration, 

2055 self._teardownDuration, 

2056 self._totalDuration, 

2057 self._status, 

2058 self._warningCount, 

2059 self._errorCount, 

2060 self._fatalCount, 

2061 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values()) 

2062 ) 

2063 

2064 testsuiteSummary._tests = self._tests 

2065 testsuiteSummary._excluded = self._excluded 

2066 testsuiteSummary._inconsistent = self._inconsistent 

2067 testsuiteSummary._skipped = self._skipped 

2068 testsuiteSummary._errored = self._errored 

2069 testsuiteSummary._weak = self._weak 

2070 testsuiteSummary._failed = self._failed 

2071 testsuiteSummary._passed = self._passed 

2072 

2073 return testsuiteSummary