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

836 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 22:23 +0000

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2024-2025 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 ): 

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: If parameter 'parent' is not a TestsuiteBase. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

338 :raises ValueError: If 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 returning the reference to the parent test entity. 

482 

483 :return: Reference to the parent entity. 

484 """ 

485 return self._parent 

486 

487 @readonly 

488 def Name(self) -> str: 

489 """ 

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

491 

492 :return: 

493 """ 

494 return self._name 

495 

496 @readonly 

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

498 """ 

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

500 

501 :return: 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 returning the duration of the test entity's setup. 

509 

510 :return: 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 returning 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 :return: 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 returning the duration of the test entity's teardown. 

530 

531 :return: 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 returning the total duration of a test entity run. 

539 

540 this duration includes setup and teardown durations. 

541 

542 :return: 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 returning the number of encountered warnings. 

550 

551 :return: Count of encountered warnings. 

552 """ 

553 return self._warningCount 

554 

555 @readonly 

556 def ErrorCount(self) -> int: 

557 """ 

558 Read-only property returning the number of encountered errors. 

559 

560 :return: Count of encountered errors. 

561 """ 

562 return self._errorCount 

563 

564 @readonly 

565 def FatalCount(self) -> int: 

566 """ 

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

568 

569 :return: Count of encountered fatal errors. 

570 """ 

571 return self._fatalCount 

572 

573 @readonly 

574 def ExpectedWarningCount(self) -> int: 

575 """ 

576 Read-only property returning the number of expected warnings. 

577 

578 :return: Count of expected warnings. 

579 """ 

580 return self._expectedWarningCount 

581 

582 @readonly 

583 def ExpectedErrorCount(self) -> int: 

584 """ 

585 Read-only property returning the number of expected errors. 

586 

587 :return: Count of expected errors. 

588 """ 

589 return self._expectedErrorCount 

590 

591 @readonly 

592 def ExpectedFatalCount(self) -> int: 

593 """ 

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

595 

596 :return: 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 :return: 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 :return: 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 :return: 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 :return: 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): 

655 """ 

656 Aggregate all test entities in the hierarchy. 

657 

658 :return: 

659 """ 

660 

661 @abstractmethod 

662 def __str__(self) -> str: 

663 """ 

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

665 """ 

666 

667 

668@export 

669class Testcase(Base): 

670 """ 

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

672 

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

674 

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

676 

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

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

679 """ 

680 

681 _status: TestcaseStatus 

682 _assertionCount: Nullable[int] 

683 _failedAssertionCount: Nullable[int] 

684 _passedAssertionCount: Nullable[int] 

685 

686 def __init__( 

687 self, 

688 name: str, 

689 startTime: Nullable[datetime] = None, 

690 setupDuration: Nullable[timedelta] = None, 

691 testDuration: Nullable[timedelta] = None, 

692 teardownDuration: Nullable[timedelta] = None, 

693 totalDuration: Nullable[timedelta] = None, 

694 status: TestcaseStatus = TestcaseStatus.Unknown, 

695 assertionCount: Nullable[int] = None, 

696 failedAssertionCount: Nullable[int] = None, 

697 passedAssertionCount: Nullable[int] = None, 

698 warningCount: int = 0, 

699 errorCount: int = 0, 

700 fatalCount: int = 0, 

701 expectedWarningCount: int = 0, 

702 expectedErrorCount: int = 0, 

703 expectedFatalCount: int = 0, 

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

705 parent: Nullable["Testsuite"] = None 

706 ): 

707 """ 

708 Initializes the fields of a test case. 

709 

710 :param name: Name of the test entity. 

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

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

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

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

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

716 :param status: Status of the test case. 

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

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

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

720 :param warningCount: Count of encountered warnings. 

721 :param errorCount: Count of encountered errors. 

722 :param fatalCount: Count of encountered fatal errors. 

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

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

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

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

727 """ 

728 

729 if parent is not None: 

730 if not isinstance(parent, Testsuite): 

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

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

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

734 raise ex 

735 

736 parent._testcases[name] = self 

737 

738 super().__init__( 

739 name, 

740 startTime, 

741 setupDuration, testDuration, teardownDuration, totalDuration, 

742 warningCount, errorCount, fatalCount, 

743 expectedWarningCount, expectedErrorCount, expectedFatalCount, 

744 keyValuePairs, 

745 parent 

746 ) 

747 

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

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

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

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

752 raise ex 

753 

754 self._status = status 

755 

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

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

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

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

760 raise ex 

761 

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

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

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

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

766 raise ex 

767 

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

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

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

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

772 raise ex 

773 

774 self._assertionCount = assertionCount 

775 if assertionCount is not None: 

776 if failedAssertionCount is not None: 

777 self._failedAssertionCount = failedAssertionCount 

778 

779 if passedAssertionCount is not None: 

780 if passedAssertionCount + failedAssertionCount != assertionCount: 

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

782 

783 self._passedAssertionCount = passedAssertionCount 

784 else: 

785 self._passedAssertionCount = assertionCount - failedAssertionCount 

786 elif passedAssertionCount is not None: 

787 self._passedAssertionCount = passedAssertionCount 

788 self._failedAssertionCount = assertionCount - passedAssertionCount 

789 else: 

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

791 elif failedAssertionCount is not None: 

792 self._failedAssertionCount = failedAssertionCount 

793 

794 if passedAssertionCount is not None: 

795 self._passedAssertionCount = passedAssertionCount 

796 self._assertionCount = passedAssertionCount + failedAssertionCount 

797 else: 

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

799 elif passedAssertionCount is not None: 

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

801 else: 

802 self._passedAssertionCount = None 

803 self._failedAssertionCount = None 

804 

805 @readonly 

806 def Status(self) -> TestcaseStatus: 

807 """ 

808 Read-only property returning the status of the test case. 

809 

810 :return: The test case's status. 

811 """ 

812 return self._status 

813 

814 @readonly 

815 def AssertionCount(self) -> int: 

816 """ 

817 Read-only property returning the number of assertions (checks) in a test case. 

818 

819 :return: Number of assertions. 

820 """ 

821 if self._assertionCount is None: 

822 return 0 

823 return self._assertionCount 

824 

825 @readonly 

826 def FailedAssertionCount(self) -> int: 

827 """ 

828 Read-only property returning the number of failed assertions (failed checks) in a test case. 

829 

830 :return: Number of assertions. 

831 """ 

832 return self._failedAssertionCount 

833 

834 @readonly 

835 def PassedAssertionCount(self) -> int: 

836 """ 

837 Read-only property returning the number of passed assertions (successful checks) in a test case. 

838 

839 :return: Number of passed assertions. 

840 """ 

841 return self._passedAssertionCount 

842 

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

844 return self.__class__( 

845 self._name, 

846 self._startTime, 

847 self._setupDuration, 

848 self._testDuration, 

849 self._teardownDuration, 

850 self._totalDuration, 

851 self._status, 

852 self._warningCount, 

853 self._errorCount, 

854 self._fatalCount, 

855 self._expectedWarningCount, 

856 self._expectedErrorCount, 

857 self._expectedFatalCount, 

858 ) 

859 # TODO: copy key-value-pairs? 

860 

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

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

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

864 self._status = TestcaseStatus.Passed 

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

866 self._status = TestcaseStatus.Weak 

867 elif self._failedAssertionCount == 0: 

868 self._status = TestcaseStatus.Passed 

869 else: 

870 self._status = TestcaseStatus.Failed 

871 

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

873 self._status |= TestcaseStatus.Warned 

874 

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

876 self._status |= TestcaseStatus.Errored 

877 

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

879 self._status |= TestcaseStatus.Aborted 

880 

881 if strict: 

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

883 

884 # TODO: check for setup errors 

885 # TODO: check for teardown errors 

886 

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

888 

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

890 

891 def __str__(self) -> str: 

892 """ 

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

894 

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

896 

897 :return: Human-readable summary of a test case object. 

898 """ 

899 return ( 

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

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

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

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

904 ) 

905 

906 

907@export 

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

909 """ 

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

911 

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

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

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

915 """ 

916 

917 _kind: TestsuiteKind 

918 _status: TestsuiteStatus 

919 _testsuites: Dict[str, TestsuiteType] 

920 

921 _tests: int 

922 _inconsistent: int 

923 _excluded: int 

924 _skipped: int 

925 _errored: int 

926 _weak: int 

927 _failed: int 

928 _passed: int 

929 

930 def __init__( 

931 self, 

932 name: str, 

933 kind: TestsuiteKind = TestsuiteKind.Logical, 

934 startTime: Nullable[datetime] = None, 

935 setupDuration: Nullable[timedelta] = None, 

936 testDuration: Nullable[timedelta] = None, 

937 teardownDuration: Nullable[timedelta] = None, 

938 totalDuration: Nullable[timedelta] = None, 

939 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

940 warningCount: int = 0, 

941 errorCount: int = 0, 

942 fatalCount: int = 0, 

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

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

945 parent: Nullable["Testsuite"] = None 

946 ): 

947 """ 

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

949 

950 :param name: Name of the test entity. 

951 :param kind: Kind of the test entity. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

969 """ 

970 if parent is not None: 

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

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

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

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

975 raise ex 

976 

977 parent._testsuites[name] = self 

978 

979 super().__init__( 

980 name, 

981 startTime, 

982 setupDuration, 

983 testDuration, 

984 teardownDuration, 

985 totalDuration, 

986 warningCount, 

987 errorCount, 

988 fatalCount, 

989 0, 0, 0, 

990 keyValuePairs, 

991 parent 

992 ) 

993 

994 self._kind = kind 

995 self._status = status 

996 

997 self._testsuites = {} 

998 if testsuites is not None: 

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

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

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

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

1003 raise ex 

1004 

1005 for testsuite in testsuites: 

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

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

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

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

1010 raise ex 

1011 

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

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

1014 

1015 if testsuite._name in self._testsuites: 

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

1017 

1018 testsuite._parent = self 

1019 self._testsuites[testsuite._name] = testsuite 

1020 

1021 self._status = TestsuiteStatus.Unknown 

1022 self._tests = 0 

1023 self._inconsistent = 0 

1024 self._excluded = 0 

1025 self._skipped = 0 

1026 self._errored = 0 

1027 self._weak = 0 

1028 self._failed = 0 

1029 self._passed = 0 

1030 

1031 @readonly 

1032 def Kind(self) -> TestsuiteKind: 

1033 """ 

1034 Read-only property returning the kind of the test suite. 

1035 

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

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

1038 language construct. 

1039 

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

1041 

1042 :return: Kind of the test suite. 

1043 """ 

1044 return self._kind 

1045 

1046 @readonly 

1047 def Status(self) -> TestsuiteStatus: 

1048 """ 

1049 Read-only property returning the aggregated overall status of the test suite. 

1050 

1051 :return: Overall status of the test suite. 

1052 """ 

1053 return self._status 

1054 

1055 @readonly 

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

1057 """ 

1058 Read-only property returning a reference to the internal dictionary of test suites. 

1059 

1060 :return: Reference to the dictionary of test suite. 

1061 """ 

1062 return self._testsuites 

1063 

1064 @readonly 

1065 def TestsuiteCount(self) -> int: 

1066 """ 

1067 Read-only property returning the number of all test suites in the test suite hierarchy. 

1068 

1069 :return: Number of test suites. 

1070 """ 

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

1072 

1073 @readonly 

1074 def TestcaseCount(self) -> int: 

1075 """ 

1076 Read-only property returning the number of all test cases in the test entity hierarchy. 

1077 

1078 :return: Number of test cases. 

1079 """ 

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

1081 

1082 @readonly 

1083 def AssertionCount(self) -> int: 

1084 """ 

1085 Read-only property returning the number of all assertions in all test cases in the test entity hierarchy. 

1086 

1087 :return: Number of assertions in all test cases. 

1088 """ 

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

1090 

1091 @readonly 

1092 def FailedAssertionCount(self) -> int: 

1093 """ 

1094 Read-only property returning the number of all failed assertions in all test cases in the test entity hierarchy. 

1095 

1096 :return: Number of failed assertions in all test cases. 

1097 """ 

1098 raise NotImplementedError() 

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

1100 

1101 @readonly 

1102 def PassedAssertionCount(self) -> int: 

1103 """ 

1104 Read-only property returning the number of all passed assertions in all test cases in the test entity hierarchy. 

1105 

1106 :return: Number of passed assertions in all test cases. 

1107 """ 

1108 raise NotImplementedError() 

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

1110 

1111 @readonly 

1112 def Tests(self) -> int: 

1113 return self._tests 

1114 

1115 @readonly 

1116 def Inconsistent(self) -> int: 

1117 """ 

1118 Read-only property returning the number of inconsistent tests in the test suite hierarchy. 

1119 

1120 :return: Number of inconsistent tests. 

1121 """ 

1122 return self._inconsistent 

1123 

1124 @readonly 

1125 def Excluded(self) -> int: 

1126 """ 

1127 Read-only property returning the number of excluded tests in the test suite hierarchy. 

1128 

1129 :return: Number of excluded tests. 

1130 """ 

1131 return self._excluded 

1132 

1133 @readonly 

1134 def Skipped(self) -> int: 

1135 """ 

1136 Read-only property returning the number of skipped tests in the test suite hierarchy. 

1137 

1138 :return: Number of skipped tests. 

1139 """ 

1140 return self._skipped 

1141 

1142 @readonly 

1143 def Errored(self) -> int: 

1144 """ 

1145 Read-only property returning the number of tests with errors in the test suite hierarchy. 

1146 

1147 :return: Number of errored tests. 

1148 """ 

1149 return self._errored 

1150 

1151 @readonly 

1152 def Weak(self) -> int: 

1153 """ 

1154 Read-only property returning the number of weak tests in the test suite hierarchy. 

1155 

1156 :return: Number of weak tests. 

1157 """ 

1158 return self._weak 

1159 

1160 @readonly 

1161 def Failed(self) -> int: 

1162 """ 

1163 Read-only property returning the number of failed tests in the test suite hierarchy. 

1164 

1165 :return: Number of failed tests. 

1166 """ 

1167 return self._failed 

1168 

1169 @readonly 

1170 def Passed(self) -> int: 

1171 """ 

1172 Read-only property returning the number of passed tests in the test suite hierarchy. 

1173 

1174 :return: Number of passed tests. 

1175 """ 

1176 return self._passed 

1177 

1178 @readonly 

1179 def WarningCount(self) -> int: 

1180 raise NotImplementedError() 

1181 # return self._warningCount 

1182 

1183 @readonly 

1184 def ErrorCount(self) -> int: 

1185 raise NotImplementedError() 

1186 # return self._errorCount 

1187 

1188 @readonly 

1189 def FatalCount(self) -> int: 

1190 raise NotImplementedError() 

1191 # return self._fatalCount 

1192 

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

1194 tests = 0 

1195 inconsistent = 0 

1196 excluded = 0 

1197 skipped = 0 

1198 errored = 0 

1199 weak = 0 

1200 failed = 0 

1201 passed = 0 

1202 

1203 warningCount = 0 

1204 errorCount = 0 

1205 fatalCount = 0 

1206 

1207 expectedWarningCount = 0 

1208 expectedErrorCount = 0 

1209 expectedFatalCount = 0 

1210 

1211 totalDuration = timedelta() 

1212 

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

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

1215 tests += t 

1216 inconsistent += i 

1217 excluded += ex 

1218 skipped += s 

1219 errored += e 

1220 weak += w 

1221 failed += f 

1222 passed += p 

1223 

1224 warningCount += wc 

1225 errorCount += ec 

1226 fatalCount += fc 

1227 

1228 expectedWarningCount += ewc 

1229 expectedErrorCount += eec 

1230 expectedFatalCount += efc 

1231 

1232 totalDuration += td 

1233 

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

1235 

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

1237 """ 

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

1239 

1240 :param testsuite: The test suite to add. 

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

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

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

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

1245 """ 

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

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

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

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

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

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

1252 raise ex 

1253 

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

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

1256 

1257 if testsuite._name in self._testsuites: 

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

1259 

1260 testsuite._parent = self 

1261 self._testsuites[testsuite._name] = testsuite 

1262 

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

1264 """ 

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

1266 

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

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

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

1270 """ 

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

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

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

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

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

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

1277 raise ex 

1278 

1279 for testsuite in testsuites: 

1280 self.AddTestsuite(testsuite) 

1281 

1282 @abstractmethod 

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

1284 pass 

1285 

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

1287 return self.Iterate(scheme) 

1288 

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

1290 return self.Iterate(scheme) 

1291 

1292 def ToTree(self) -> Node: 

1293 rootNode = Node(value=self._name) 

1294 

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

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

1297 

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

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

1300 

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

1302 convertTestsuite(ts, testsuiteNode) 

1303 

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

1305 convertTestcase(tc, testsuiteNode) 

1306 

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

1308 convertTestsuite(testsuite, rootNode) 

1309 

1310 return rootNode 

1311 

1312 

1313@export 

1314class Testsuite(TestsuiteBase[TestsuiteType]): 

1315 """ 

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

1317 

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

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

1320 """ 

1321 

1322 _testcases: Dict[str, "Testcase"] 

1323 

1324 def __init__( 

1325 self, 

1326 name: str, 

1327 kind: TestsuiteKind = TestsuiteKind.Logical, 

1328 startTime: Nullable[datetime] = None, 

1329 setupDuration: Nullable[timedelta] = None, 

1330 testDuration: Nullable[timedelta] = None, 

1331 teardownDuration: Nullable[timedelta] = None, 

1332 totalDuration: Nullable[timedelta] = None, 

1333 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1334 warningCount: int = 0, 

1335 errorCount: int = 0, 

1336 fatalCount: int = 0, 

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

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

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

1340 parent: Nullable[TestsuiteType] = None 

1341 ): 

1342 """ 

1343 Initializes the fields of a test suite. 

1344 

1345 :param name: Name of the test suite. 

1346 :param kind: Kind of the test suite. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1364 """ 

1365 super().__init__( 

1366 name, 

1367 kind, 

1368 startTime, 

1369 setupDuration, 

1370 testDuration, 

1371 teardownDuration, 

1372 totalDuration, 

1373 status, 

1374 warningCount, 

1375 errorCount, 

1376 fatalCount, 

1377 testsuites, 

1378 keyValuePairs, 

1379 parent 

1380 ) 

1381 

1382 # self._testDuration = testDuration 

1383 

1384 self._testcases = {} 

1385 if testcases is not None: 

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

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

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

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

1390 raise ex 

1391 

1392 for testcase in testcases: 

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

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

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

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

1397 raise ex 

1398 

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

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

1401 

1402 if testcase._name in self._testcases: 

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

1404 

1405 testcase._parent = self 

1406 self._testcases[testcase._name] = testcase 

1407 

1408 @readonly 

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

1410 """ 

1411 Read-only property returning a reference to the internal dictionary of test cases. 

1412 

1413 :return: Reference to the dictionary of test cases. 

1414 """ 

1415 return self._testcases 

1416 

1417 @readonly 

1418 def TestcaseCount(self) -> int: 

1419 """ 

1420 Read-only property returning the number of all test cases in the test entity hierarchy. 

1421 

1422 :return: Number of test cases. 

1423 """ 

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

1425 

1426 @readonly 

1427 def AssertionCount(self) -> int: 

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

1429 

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

1431 return self.__class__( 

1432 self._name, 

1433 self._startTime, 

1434 self._setupDuration, 

1435 self._teardownDuration, 

1436 self._totalDuration, 

1437 self._status, 

1438 self._warningCount, 

1439 self._errorCount, 

1440 self._fatalCount 

1441 ) 

1442 

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

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

1445 

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

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

1448 

1449 tests += 1 

1450 

1451 warningCount += wc 

1452 errorCount += ec 

1453 fatalCount += fc 

1454 

1455 expectedWarningCount += ewc 

1456 expectedErrorCount += eec 

1457 expectedFatalCount += efc 

1458 

1459 totalDuration += td 

1460 

1461 status = testcase._status 

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

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

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

1465 inconsistent += 1 

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

1467 excluded += 1 

1468 elif status is TestcaseStatus.Skipped: 

1469 skipped += 1 

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

1471 errored += 1 

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

1473 weak += 1 

1474 elif status is TestcaseStatus.Passed: 

1475 passed += 1 

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

1477 failed += 1 

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

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

1480 else: 

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

1482 

1483 self._tests = tests 

1484 self._inconsistent = inconsistent 

1485 self._excluded = excluded 

1486 self._skipped = skipped 

1487 self._errored = errored 

1488 self._weak = weak 

1489 self._failed = failed 

1490 self._passed = passed 

1491 

1492 self._warningCount = warningCount 

1493 self._errorCount = errorCount 

1494 self._fatalCount = fatalCount 

1495 

1496 self._expectedWarningCount = expectedWarningCount 

1497 self._expectedErrorCount = expectedErrorCount 

1498 self._expectedFatalCount = expectedFatalCount 

1499 

1500 if self._totalDuration is None: 

1501 self._totalDuration = totalDuration 

1502 

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

1504 self._status = TestsuiteStatus.Errored 

1505 elif failed > 0: 

1506 self._status = TestsuiteStatus.Failed 

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

1508 self._status = TestsuiteStatus.Empty 

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

1510 self._status = TestsuiteStatus.Passed 

1511 elif tests == skipped: 

1512 self._status = TestsuiteStatus.Skipped 

1513 else: 

1514 self._status = TestsuiteStatus.Unknown 

1515 

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

1517 

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

1519 """ 

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

1521 

1522 :param testcase: The test case to add. 

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

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

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

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

1527 """ 

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

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

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

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

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

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

1534 raise ex 

1535 

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

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

1538 

1539 if testcase._name in self._testcases: 

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

1541 

1542 testcase._parent = self 

1543 self._testcases[testcase._name] = testcase 

1544 

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

1546 """ 

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

1548 

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

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

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

1552 """ 

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

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

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

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

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

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

1559 raise ex 

1560 

1561 for testcase in testcases: 

1562 self.AddTestcase(testcase) 

1563 

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

1565 assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme 

1566 

1567 if IterationScheme.PreOrder in scheme: 

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

1569 yield self 

1570 

1571 if IterationScheme.IncludeTestcases in scheme: 

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

1573 yield testcase 

1574 

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

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

1577 

1578 if IterationScheme.PostOrder in scheme: 

1579 if IterationScheme.IncludeTestcases in scheme: 

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

1581 yield testcase 

1582 

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

1584 yield self 

1585 

1586 def __str__(self) -> str: 

1587 return ( 

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

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

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

1591 ) 

1592 

1593 

1594@export 

1595class TestsuiteSummary(TestsuiteBase[TestsuiteType]): 

1596 """ 

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

1598 

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

1600 """ 

1601 

1602 def __init__( 

1603 self, 

1604 name: str, 

1605 startTime: Nullable[datetime] = None, 

1606 setupDuration: Nullable[timedelta] = None, 

1607 testDuration: Nullable[timedelta] = None, 

1608 teardownDuration: Nullable[timedelta] = None, 

1609 totalDuration: Nullable[timedelta] = None, 

1610 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1611 warningCount: int = 0, 

1612 errorCount: int = 0, 

1613 fatalCount: int = 0, 

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

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

1616 parent: Nullable[TestsuiteType] = None 

1617 ) -> None: 

1618 """ 

1619 Initializes the fields of a test summary. 

1620 

1621 :param name: Name of the test summary. 

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

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

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

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

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

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

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

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

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

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

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

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

1634 """ 

1635 super().__init__( 

1636 name, 

1637 TestsuiteKind.Root, 

1638 startTime, setupDuration, testDuration, teardownDuration, totalDuration, 

1639 status, 

1640 warningCount, errorCount, fatalCount, 

1641 testsuites, 

1642 keyValuePairs, 

1643 parent 

1644 ) 

1645 

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

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

1648 

1649 self._tests = tests 

1650 self._inconsistent = inconsistent 

1651 self._excluded = excluded 

1652 self._skipped = skipped 

1653 self._errored = errored 

1654 self._weak = weak 

1655 self._failed = failed 

1656 self._passed = passed 

1657 

1658 self._warningCount = warningCount 

1659 self._errorCount = errorCount 

1660 self._fatalCount = fatalCount 

1661 

1662 self._expectedWarningCount = expectedWarningCount 

1663 self._expectedErrorCount = expectedErrorCount 

1664 self._expectedFatalCount = expectedFatalCount 

1665 

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

1667 self._totalDuration = totalDuration 

1668 

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

1670 self._status = TestsuiteStatus.Errored 

1671 elif failed > 0: 

1672 self._status = TestsuiteStatus.Failed 

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

1674 self._status = TestsuiteStatus.Empty 

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

1676 self._status = TestsuiteStatus.Passed 

1677 elif tests == skipped: 

1678 self._status = TestsuiteStatus.Skipped 

1679 elif tests == excluded: 

1680 self._status = TestsuiteStatus.Excluded 

1681 else: 

1682 self._status = TestsuiteStatus.Unknown 

1683 

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

1685 

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

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

1688 yield self 

1689 

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

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

1692 

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

1694 yield self 

1695 

1696 def __str__(self) -> str: 

1697 return ( 

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

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

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

1701 ) 

1702 

1703 

1704@export 

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

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

1707 

1708 _path: Path 

1709 

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

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

1712 

1713 def __init__(self, reportFile: Path, analyzeAndConvert: bool = False): 

1714 self._path = reportFile 

1715 

1716 self._analysisDuration = -1.0 

1717 self._modelConversion = -1.0 

1718 

1719 if analyzeAndConvert: 

1720 self.Analyze() 

1721 self.Convert() 

1722 

1723 @readonly 

1724 def Path(self) -> Path: 

1725 """ 

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

1727 

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

1729 """ 

1730 return self._path 

1731 

1732 @readonly 

1733 def AnalysisDuration(self) -> timedelta: 

1734 """ 

1735 Read-only property returning analysis duration. 

1736 

1737 .. note:: 

1738 

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

1740 content to the test entity hierarchy. 

1741 

1742 :return: Duration to analyze the document. 

1743 """ 

1744 return timedelta(seconds=self._analysisDuration) 

1745 

1746 @readonly 

1747 def ModelConversionDuration(self) -> timedelta: 

1748 """ 

1749 Read-only property returning conversion duration. 

1750 

1751 .. note:: 

1752 

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

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

1755 

1756 :return: Duration to convert the document. 

1757 """ 

1758 return timedelta(seconds=self._modelConversion) 

1759 

1760 @abstractmethod 

1761 def Analyze(self) -> None: 

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

1763 

1764 # @abstractmethod 

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

1766 # pass 

1767 

1768 @abstractmethod 

1769 def Convert(self): 

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

1771 

1772 

1773@export 

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

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

1776 

1777 _mergedCount: int 

1778 

1779 def __init__(self, mergedCount: int = 1): 

1780 self._mergedCount = mergedCount 

1781 

1782 @readonly 

1783 def MergedCount(self) -> int: 

1784 return self._mergedCount 

1785 

1786 

1787@export 

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

1789 _combinedCount: int 

1790 

1791 def __init__(self, combinedCound: int = 1): 

1792 self._combinedCount = combinedCound 

1793 

1794 @readonly 

1795 def CombinedCount(self) -> int: 

1796 return self._combinedCount 

1797 

1798 

1799@export 

1800class MergedTestcase(Testcase, Merged): 

1801 _mergedTestcases: List[Testcase] 

1802 

1803 def __init__( 

1804 self, 

1805 testcase: Testcase, 

1806 parent: Nullable["Testsuite"] = None 

1807 ): 

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

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

1810 

1811 super().__init__( 

1812 testcase._name, 

1813 testcase._startTime, 

1814 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration, 

1815 TestcaseStatus.Unknown, 

1816 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount, 

1817 testcase._warningCount, testcase._errorCount, testcase._fatalCount, 

1818 testcase._expectedWarningCount, testcase._expectedErrorCount, testcase._expectedFatalCount, 

1819 parent 

1820 ) 

1821 Merged.__init__(self) 

1822 

1823 self._mergedTestcases = [testcase] 

1824 

1825 @readonly 

1826 def Status(self) -> TestcaseStatus: 

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

1828 status = self._mergedTestcases[0]._status 

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

1830 status @= mtc._status 

1831 

1832 self._status = status 

1833 

1834 return self._status 

1835 

1836 @readonly 

1837 def SummedAssertionCount(self) -> int: 

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

1839 

1840 @readonly 

1841 def SummedPassedAssertionCount(self) -> int: 

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

1843 

1844 @readonly 

1845 def SummedFailedAssertionCount(self) -> int: 

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

1847 

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

1849 firstMTC = self._mergedTestcases[0] 

1850 

1851 status = firstMTC._status 

1852 warningCount = firstMTC._warningCount 

1853 errorCount = firstMTC._errorCount 

1854 fatalCount = firstMTC._fatalCount 

1855 totalDuration = firstMTC._totalDuration 

1856 

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

1858 status @= mtc._status 

1859 warningCount += mtc._warningCount 

1860 errorCount += mtc._errorCount 

1861 fatalCount += mtc._fatalCount 

1862 

1863 self._status = status 

1864 

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

1866 

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

1868 self._mergedCount += 1 

1869 

1870 self._mergedTestcases.append(tc) 

1871 

1872 self._warningCount += tc._warningCount 

1873 self._errorCount += tc._errorCount 

1874 self._fatalCount += tc._fatalCount 

1875 

1876 def ToTestcase(self) -> Testcase: 

1877 return Testcase( 

1878 self._name, 

1879 self._startTime, 

1880 self._setupDuration, 

1881 self._testDuration, 

1882 self._teardownDuration, 

1883 self._totalDuration, 

1884 self._status, 

1885 self._assertionCount, 

1886 self._failedAssertionCount, 

1887 self._passedAssertionCount, 

1888 self._warningCount, 

1889 self._errorCount, 

1890 self._fatalCount 

1891 ) 

1892 

1893 

1894@export 

1895class MergedTestsuite(Testsuite, Merged): 

1896 def __init__( 

1897 self, 

1898 testsuite: Testsuite, 

1899 addTestsuites: bool = False, 

1900 addTestcases: bool = False, 

1901 parent: Nullable["Testsuite"] = None 

1902 ): 

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

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

1905 

1906 super().__init__( 

1907 testsuite._name, 

1908 testsuite._kind, 

1909 testsuite._startTime, 

1910 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration, 

1911 TestsuiteStatus.Unknown, 

1912 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount, 

1913 parent 

1914 ) 

1915 Merged.__init__(self) 

1916 

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

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

1919 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases) 

1920 self.AddTestsuite(mergedTestsuite) 

1921 

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

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

1924 mergedTestcase = MergedTestcase(tc) 

1925 self.AddTestcase(mergedTestcase) 

1926 

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

1928 self._mergedCount += 1 

1929 

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

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

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

1933 else: 

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

1935 self.AddTestsuite(mergedTestsuite) 

1936 

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

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

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

1940 else: 

1941 mergedTestcase = MergedTestcase(tc) 

1942 self.AddTestcase(mergedTestcase) 

1943 

1944 def ToTestsuite(self) -> Testsuite: 

1945 testsuite = Testsuite( 

1946 self._name, 

1947 self._kind, 

1948 self._startTime, 

1949 self._setupDuration, 

1950 self._testDuration, 

1951 self._teardownDuration, 

1952 self._totalDuration, 

1953 self._status, 

1954 self._warningCount, 

1955 self._errorCount, 

1956 self._fatalCount, 

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

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

1959 ) 

1960 

1961 testsuite._tests = self._tests 

1962 testsuite._excluded = self._excluded 

1963 testsuite._inconsistent = self._inconsistent 

1964 testsuite._skipped = self._skipped 

1965 testsuite._errored = self._errored 

1966 testsuite._weak = self._weak 

1967 testsuite._failed = self._failed 

1968 testsuite._passed = self._passed 

1969 

1970 return testsuite 

1971 

1972 

1973@export 

1974class MergedTestsuiteSummary(TestsuiteSummary, Merged): 

1975 _mergedFiles: Dict[Path, TestsuiteSummary] 

1976 

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

1978 super().__init__(name) 

1979 Merged.__init__(self, mergedCount=0) 

1980 

1981 self._mergedFiles = {} 

1982 

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

1984 # if summary.File in self._mergedFiles: 

1985 # raise 

1986 

1987 # FIXME: a summary is not necessarily a file 

1988 self._mergedCount += 1 

1989 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary 

1990 

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

1992 if testsuite._name in self._testsuites: 

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

1994 else: 

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

1996 self.AddTestsuite(mergedTestsuite) 

1997 

1998 def ToTestsuiteSummary(self) -> TestsuiteSummary: 

1999 testsuiteSummary = TestsuiteSummary( 

2000 self._name, 

2001 self._startTime, 

2002 self._setupDuration, 

2003 self._testDuration, 

2004 self._teardownDuration, 

2005 self._totalDuration, 

2006 self._status, 

2007 self._warningCount, 

2008 self._errorCount, 

2009 self._fatalCount, 

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

2011 ) 

2012 

2013 testsuiteSummary._tests = self._tests 

2014 testsuiteSummary._excluded = self._excluded 

2015 testsuiteSummary._inconsistent = self._inconsistent 

2016 testsuiteSummary._skipped = self._skipped 

2017 testsuiteSummary._errored = self._errored 

2018 testsuiteSummary._weak = self._weak 

2019 testsuiteSummary._failed = self._failed 

2020 testsuiteSummary._passed = self._passed 

2021 

2022 return testsuiteSummary