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

797 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-16 22:20 +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, timedelta] 

248TestsuiteAggregateReturnType = Tuple[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 _dict: Dict[str, Any] 

287 

288 def __init__( 

289 self, 

290 name: str, 

291 startTime: Nullable[datetime] = None, 

292 setupDuration: Nullable[timedelta] = None, 

293 testDuration: Nullable[timedelta] = None, 

294 teardownDuration: Nullable[timedelta] = None, 

295 totalDuration: Nullable[timedelta] = None, 

296 warningCount: int = 0, 

297 errorCount: int = 0, 

298 fatalCount: int = 0, 

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

300 parent: Nullable["TestsuiteBase"] = None 

301 ): 

302 """ 

303 Initializes the fields of the base-class. 

304 

305 :param name: Name of the test entity. 

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

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

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

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

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

311 :param warningCount: Count of encountered warnings. 

312 :param errorCount: Count of encountered errors. 

313 :param fatalCount: Count of encountered fatal errors. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

328 :raises ValueError: If parameter 'totalDuration' is not consistent. 

329 """ 

330 

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

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

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

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

335 raise ex 

336 

337 if name is None: 

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

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

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

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

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

343 raise ex 

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

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

346 

347 self._parent = parent 

348 self._name = name 

349 

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

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

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

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

354 raise ex 

355 

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

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

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

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

360 raise ex 

361 

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

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

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

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

366 raise ex 

367 

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

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

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

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

372 raise ex 

373 

374 if testDuration is not None: 

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

376 if teardownDuration is not None: 

377 if totalDuration is not None: 

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

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

380 else: # no total 

381 totalDuration = setupDuration + testDuration + teardownDuration 

382 # no teardown 

383 elif totalDuration is not None: 

384 if totalDuration < (setupDuration + testDuration): 

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

386 # no teardown, no total 

387 else: 

388 totalDuration = setupDuration + testDuration 

389 # no setup 

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

391 if totalDuration is not None: 

392 if totalDuration < (testDuration + teardownDuration): 

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

394 else: # no setup, no total 

395 totalDuration = testDuration + teardownDuration 

396 # no setup, no teardown 

397 elif totalDuration is not None: 

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

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

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

401 totalDuration = testDuration 

402 # no test 

403 elif totalDuration is not None: 

404 testDuration = totalDuration 

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

406 testDuration -= setupDuration 

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

408 testDuration -= teardownDuration 

409 

410 self._startTime = startTime 

411 self._setupDuration = setupDuration 

412 self._testDuration = testDuration 

413 self._teardownDuration = teardownDuration 

414 self._totalDuration = totalDuration 

415 

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

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

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

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

420 raise ex 

421 

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

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

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

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

426 raise ex 

427 

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

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

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

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

432 raise ex 

433 

434 self._warningCount = warningCount 

435 self._errorCount = errorCount 

436 self._fatalCount = fatalCount 

437 

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

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

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

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

442 raise ex 

443 

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

445 

446 # QUESTION: allow Parent as setter? 

447 @readonly 

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

449 """ 

450 Read-only property returning the reference to the parent test entity. 

451 

452 :return: Reference to the parent entity. 

453 """ 

454 return self._parent 

455 

456 @readonly 

457 def Name(self) -> str: 

458 """ 

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

460 

461 :return: 

462 """ 

463 return self._name 

464 

465 @readonly 

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

467 """ 

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

469 

470 :return: Time when the test entity was started. 

471 """ 

472 return self._startTime 

473 

474 @readonly 

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

476 """ 

477 Read-only property returning the duration of the test entity's setup. 

478 

479 :return: Duration it took to set up the entity. 

480 """ 

481 return self._setupDuration 

482 

483 @readonly 

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

485 """ 

486 Read-only property returning the duration of a test entities run. 

487 

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

489 distinguishable, assign setup and teardown durations with zero. 

490 

491 :return: Duration of the entity's test run. 

492 """ 

493 return self._testDuration 

494 

495 @readonly 

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

497 """ 

498 Read-only property returning the duration of the test entity's teardown. 

499 

500 :return: Duration it took to tear down the entity. 

501 """ 

502 return self._teardownDuration 

503 

504 @readonly 

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

506 """ 

507 Read-only property returning the total duration of a test entity run. 

508 

509 this duration includes setup and teardown durations. 

510 

511 :return: Total duration of the entity's execution (setup + test + teardown) 

512 """ 

513 return self._totalDuration 

514 

515 @readonly 

516 def WarningCount(self) -> int: 

517 """ 

518 Read-only property returning the number of encountered warnings. 

519 

520 :return: Count of encountered warnings. 

521 """ 

522 return self._warningCount 

523 

524 @readonly 

525 def ErrorCount(self) -> int: 

526 """ 

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

528 

529 :return: Count of encountered errors. 

530 """ 

531 return self._errorCount 

532 

533 @readonly 

534 def FatalCount(self) -> int: 

535 """ 

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

537 

538 :return: Count of encountered fatal errors. 

539 """ 

540 return self._fatalCount 

541 

542 def __len__(self) -> int: 

543 """ 

544 Returns the number of annotated key-value pairs. 

545 

546 :return: Number of annotated key-value pairs. 

547 """ 

548 return len(self._dict) 

549 

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

551 """ 

552 Access a key-value pair by key. 

553 

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

555 :return: Value of the accessed key. 

556 """ 

557 return self._dict[key] 

558 

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

560 """ 

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

562 

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

564 

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

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

567 """ 

568 self._dict[key] = value 

569 

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

571 """ 

572 Delete a key-value pair by key. 

573 

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

575 """ 

576 del self._dict[key] 

577 

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

579 """ 

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

581 

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

583 :return: True, if the pair was annotated. 

584 """ 

585 return key in self._dict 

586 

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

588 """ 

589 Iterate all annotated key-value pairs. 

590 

591 :return: A generator of key-value pair tuples (key, value). 

592 """ 

593 yield from self._dict.items() 

594 

595 @abstractmethod 

596 def Aggregate(self, strict: bool = True): 

597 """ 

598 Aggregate all test entities in the hierarchy. 

599 

600 :return: 

601 """ 

602 

603 @abstractmethod 

604 def __str__(self) -> str: 

605 """ 

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

607 """ 

608 

609 

610@export 

611class Testcase(Base): 

612 """ 

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

614 

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

616 

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

618 

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

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

621 """ 

622 

623 _status: TestcaseStatus 

624 _assertionCount: Nullable[int] 

625 _failedAssertionCount: Nullable[int] 

626 _passedAssertionCount: Nullable[int] 

627 

628 def __init__( 

629 self, 

630 name: str, 

631 startTime: Nullable[datetime] = None, 

632 setupDuration: Nullable[timedelta] = None, 

633 testDuration: Nullable[timedelta] = None, 

634 teardownDuration: Nullable[timedelta] = None, 

635 totalDuration: Nullable[timedelta] = None, 

636 status: TestcaseStatus = TestcaseStatus.Unknown, 

637 assertionCount: Nullable[int] = None, 

638 failedAssertionCount: Nullable[int] = None, 

639 passedAssertionCount: Nullable[int] = None, 

640 warningCount: int = 0, 

641 errorCount: int = 0, 

642 fatalCount: int = 0, 

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

644 parent: Nullable["Testsuite"] = None 

645 ): 

646 """ 

647 Initializes the fields of a test case. 

648 

649 :param name: Name of the test entity. 

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

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

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

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

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

655 :param status: Status of the test case. 

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

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

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

659 :param warningCount: Count of encountered warnings. 

660 :param errorCount: Count of encountered errors. 

661 :param fatalCount: Count of encountered fatal errors. 

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

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

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

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

666 """ 

667 

668 if parent is not None: 

669 if not isinstance(parent, Testsuite): 

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

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

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

673 raise ex 

674 

675 parent._testcases[name] = self 

676 

677 super().__init__( 

678 name, 

679 startTime, 

680 setupDuration, 

681 testDuration, 

682 teardownDuration, 

683 totalDuration, 

684 warningCount, 

685 errorCount, 

686 fatalCount, 

687 keyValuePairs, 

688 parent 

689 ) 

690 

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

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

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

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

695 raise ex 

696 

697 self._status = status 

698 

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

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

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

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

703 raise ex 

704 

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

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

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

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

709 raise ex 

710 

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

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

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

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

715 raise ex 

716 

717 self._assertionCount = assertionCount 

718 if assertionCount is not None: 

719 if failedAssertionCount is not None: 

720 self._failedAssertionCount = failedAssertionCount 

721 

722 if passedAssertionCount is not None: 

723 if passedAssertionCount + failedAssertionCount != assertionCount: 

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

725 

726 self._passedAssertionCount = passedAssertionCount 

727 else: 

728 self._passedAssertionCount = assertionCount - failedAssertionCount 

729 elif passedAssertionCount is not None: 

730 self._passedAssertionCount = passedAssertionCount 

731 self._failedAssertionCount = assertionCount - passedAssertionCount 

732 else: 

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

734 elif failedAssertionCount is not None: 

735 self._failedAssertionCount = failedAssertionCount 

736 

737 if passedAssertionCount is not None: 

738 self._passedAssertionCount = passedAssertionCount 

739 self._assertionCount = passedAssertionCount + failedAssertionCount 

740 else: 

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

742 elif passedAssertionCount is not None: 

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

744 else: 

745 self._passedAssertionCount = None 

746 self._failedAssertionCount = None 

747 

748 @readonly 

749 def Status(self) -> TestcaseStatus: 

750 """ 

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

752 

753 :return: The test case's status. 

754 """ 

755 return self._status 

756 

757 @readonly 

758 def AssertionCount(self) -> int: 

759 """ 

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

761 

762 :return: Number of assertions. 

763 """ 

764 if self._assertionCount is None: 

765 return 0 

766 return self._assertionCount 

767 

768 @readonly 

769 def FailedAssertionCount(self) -> int: 

770 """ 

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

772 

773 :return: Number of assertions. 

774 """ 

775 return self._failedAssertionCount 

776 

777 @readonly 

778 def PassedAssertionCount(self) -> int: 

779 """ 

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

781 

782 :return: Number of passed assertions. 

783 """ 

784 return self._passedAssertionCount 

785 

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

787 return self.__class__( 

788 self._name, 

789 self._startTime, 

790 self._setupDuration, 

791 self._testDuration, 

792 self._teardownDuration, 

793 self._totalDuration, 

794 self._status, 

795 self._warningCount, 

796 self._errorCount, 

797 self._fatalCount, 

798 ) 

799 

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

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

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

803 self._status = TestcaseStatus.Passed 

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

805 self._status = TestcaseStatus.Weak 

806 elif self._failedAssertionCount == 0: 

807 self._status = TestcaseStatus.Passed 

808 else: 

809 self._status = TestcaseStatus.Failed 

810 

811 if self._warningCount > 0: 811 ↛ 812line 811 didn't jump to line 812 because the condition on line 811 was never true

812 self._status |= TestcaseStatus.Warned 

813 

814 if self._errorCount > 0: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true

815 self._status |= TestcaseStatus.Errored 

816 

817 if self._fatalCount > 0: 817 ↛ 818line 817 didn't jump to line 818 because the condition on line 817 was never true

818 self._status |= TestcaseStatus.Aborted 

819 

820 if strict: 

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

822 

823 # TODO: check for setup errors 

824 # TODO: check for teardown errors 

825 

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

827 

828 return self._warningCount, self._errorCount, self._fatalCount, totalDuration 

829 

830 def __str__(self) -> str: 

831 """ 

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

833 

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

835 

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

837 """ 

838 return ( 

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

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

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

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

843 ) 

844 

845 

846@export 

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

848 """ 

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

850 

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

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

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

854 """ 

855 

856 _kind: TestsuiteKind 

857 _status: TestsuiteStatus 

858 _testsuites: Dict[str, TestsuiteType] 

859 

860 _tests: int 

861 _inconsistent: int 

862 _excluded: int 

863 _skipped: int 

864 _errored: int 

865 _weak: int 

866 _failed: int 

867 _passed: int 

868 

869 def __init__( 

870 self, 

871 name: str, 

872 kind: TestsuiteKind = TestsuiteKind.Logical, 

873 startTime: Nullable[datetime] = None, 

874 setupDuration: Nullable[timedelta] = None, 

875 testDuration: Nullable[timedelta] = None, 

876 teardownDuration: Nullable[timedelta] = None, 

877 totalDuration: Nullable[timedelta] = None, 

878 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

879 warningCount: int = 0, 

880 errorCount: int = 0, 

881 fatalCount: int = 0, 

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

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

884 parent: Nullable["Testsuite"] = None 

885 ): 

886 """ 

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

888 

889 :param name: Name of the test entity. 

890 :param kind: Kind of the test entity. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

908 """ 

909 if parent is not None: 

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

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

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

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

914 raise ex 

915 

916 parent._testsuites[name] = self 

917 

918 super().__init__( 

919 name, 

920 startTime, 

921 setupDuration, 

922 testDuration, 

923 teardownDuration, 

924 totalDuration, 

925 warningCount, 

926 errorCount, 

927 fatalCount, 

928 keyValuePairs, 

929 parent 

930 ) 

931 

932 self._kind = kind 

933 self._status = status 

934 

935 self._testsuites = {} 

936 if testsuites is not None: 

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

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

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

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

941 raise ex 

942 

943 for testsuite in testsuites: 

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

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

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

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

948 raise ex 

949 

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

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

952 

953 if testsuite._name in self._testsuites: 

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

955 

956 testsuite._parent = self 

957 self._testsuites[testsuite._name] = testsuite 

958 

959 self._status = TestsuiteStatus.Unknown 

960 self._tests = 0 

961 self._inconsistent = 0 

962 self._excluded = 0 

963 self._skipped = 0 

964 self._errored = 0 

965 self._weak = 0 

966 self._failed = 0 

967 self._passed = 0 

968 

969 @readonly 

970 def Kind(self) -> TestsuiteKind: 

971 """ 

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

973 

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

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

976 language construct. 

977 

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

979 

980 :return: Kind of the test suite. 

981 """ 

982 return self._kind 

983 

984 @readonly 

985 def Status(self) -> TestsuiteStatus: 

986 """ 

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

988 

989 :return: Overall status of the test suite. 

990 """ 

991 return self._status 

992 

993 @readonly 

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

995 """ 

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

997 

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

999 """ 

1000 return self._testsuites 

1001 

1002 @readonly 

1003 def TestsuiteCount(self) -> int: 

1004 """ 

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

1006 

1007 :return: Number of test suites. 

1008 """ 

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

1010 

1011 @readonly 

1012 def TestcaseCount(self) -> int: 

1013 """ 

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

1015 

1016 :return: Number of test cases. 

1017 """ 

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

1019 

1020 @readonly 

1021 def AssertionCount(self) -> int: 

1022 """ 

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

1024 

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

1026 """ 

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

1028 

1029 @readonly 

1030 def FailedAssertionCount(self) -> int: 

1031 """ 

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

1033 

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

1035 """ 

1036 raise NotImplementedError() 

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

1038 

1039 @readonly 

1040 def PassedAssertionCount(self) -> int: 

1041 """ 

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

1043 

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

1045 """ 

1046 raise NotImplementedError() 

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

1048 

1049 @readonly 

1050 def Tests(self) -> int: 

1051 return self._tests 

1052 

1053 @readonly 

1054 def Inconsistent(self) -> int: 

1055 """ 

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

1057 

1058 :return: Number of inconsistent tests. 

1059 """ 

1060 return self._inconsistent 

1061 

1062 @readonly 

1063 def Excluded(self) -> int: 

1064 """ 

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

1066 

1067 :return: Number of excluded tests. 

1068 """ 

1069 return self._excluded 

1070 

1071 @readonly 

1072 def Skipped(self) -> int: 

1073 """ 

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

1075 

1076 :return: Number of skipped tests. 

1077 """ 

1078 return self._skipped 

1079 

1080 @readonly 

1081 def Errored(self) -> int: 

1082 """ 

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

1084 

1085 :return: Number of errored tests. 

1086 """ 

1087 return self._errored 

1088 

1089 @readonly 

1090 def Weak(self) -> int: 

1091 """ 

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

1093 

1094 :return: Number of weak tests. 

1095 """ 

1096 return self._weak 

1097 

1098 @readonly 

1099 def Failed(self) -> int: 

1100 """ 

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

1102 

1103 :return: Number of failed tests. 

1104 """ 

1105 return self._failed 

1106 

1107 @readonly 

1108 def Passed(self) -> int: 

1109 """ 

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

1111 

1112 :return: Number of passed tests. 

1113 """ 

1114 return self._passed 

1115 

1116 @readonly 

1117 def WarningCount(self) -> int: 

1118 raise NotImplementedError() 

1119 # return self._warningCount 

1120 

1121 @readonly 

1122 def ErrorCount(self) -> int: 

1123 raise NotImplementedError() 

1124 # return self._errorCount 

1125 

1126 @readonly 

1127 def FatalCount(self) -> int: 

1128 raise NotImplementedError() 

1129 # return self._fatalCount 

1130 

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

1132 tests = 0 

1133 inconsistent = 0 

1134 excluded = 0 

1135 skipped = 0 

1136 errored = 0 

1137 weak = 0 

1138 failed = 0 

1139 passed = 0 

1140 

1141 warningCount = 0 

1142 errorCount = 0 

1143 fatalCount = 0 

1144 

1145 totalDuration = timedelta() 

1146 

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

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

1149 tests += t 

1150 inconsistent += i 

1151 excluded += ex 

1152 skipped += s 

1153 errored += e 

1154 weak += w 

1155 failed += f 

1156 passed += p 

1157 

1158 warningCount += wc 

1159 errorCount += ec 

1160 fatalCount += fc 

1161 

1162 totalDuration += td 

1163 

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

1165 

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

1167 """ 

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

1169 

1170 :param testsuite: The test suite to add. 

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

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

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

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

1175 """ 

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

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

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

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

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

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

1182 raise ex 

1183 

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

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

1186 

1187 if testsuite._name in self._testsuites: 

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

1189 

1190 testsuite._parent = self 

1191 self._testsuites[testsuite._name] = testsuite 

1192 

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

1194 """ 

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

1196 

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

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

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

1200 """ 

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

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

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

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

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

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

1207 raise ex 

1208 

1209 for testsuite in testsuites: 

1210 self.AddTestsuite(testsuite) 

1211 

1212 @abstractmethod 

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

1214 pass 

1215 

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

1217 return self.Iterate(scheme) 

1218 

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

1220 return self.Iterate(scheme) 

1221 

1222 def ToTree(self) -> Node: 

1223 rootNode = Node(value=self._name) 

1224 

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

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

1227 

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

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

1230 

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

1232 convertTestsuite(ts, testsuiteNode) 

1233 

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

1235 convertTestcase(tc, testsuiteNode) 

1236 

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

1238 convertTestsuite(testsuite, rootNode) 

1239 

1240 return rootNode 

1241 

1242 

1243@export 

1244class Testsuite(TestsuiteBase[TestsuiteType]): 

1245 """ 

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

1247 

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

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

1250 """ 

1251 

1252 _testcases: Dict[str, "Testcase"] 

1253 

1254 def __init__( 

1255 self, 

1256 name: str, 

1257 kind: TestsuiteKind = TestsuiteKind.Logical, 

1258 startTime: Nullable[datetime] = None, 

1259 setupDuration: Nullable[timedelta] = None, 

1260 testDuration: Nullable[timedelta] = None, 

1261 teardownDuration: Nullable[timedelta] = None, 

1262 totalDuration: Nullable[timedelta] = None, 

1263 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1264 warningCount: int = 0, 

1265 errorCount: int = 0, 

1266 fatalCount: int = 0, 

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

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

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

1270 parent: Nullable[TestsuiteType] = None 

1271 ): 

1272 """ 

1273 Initializes the fields of a test suite. 

1274 

1275 :param name: Name of the test suite. 

1276 :param kind: Kind of the test suite. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1294 """ 

1295 super().__init__( 

1296 name, 

1297 kind, 

1298 startTime, 

1299 setupDuration, 

1300 testDuration, 

1301 teardownDuration, 

1302 totalDuration, 

1303 status, 

1304 warningCount, 

1305 errorCount, 

1306 fatalCount, 

1307 testsuites, 

1308 keyValuePairs, 

1309 parent 

1310 ) 

1311 

1312 # self._testDuration = testDuration 

1313 

1314 self._testcases = {} 

1315 if testcases is not None: 

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

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

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

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

1320 raise ex 

1321 

1322 for testcase in testcases: 

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

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

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

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

1327 raise ex 

1328 

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

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

1331 

1332 if testcase._name in self._testcases: 

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

1334 

1335 testcase._parent = self 

1336 self._testcases[testcase._name] = testcase 

1337 

1338 @readonly 

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

1340 """ 

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

1342 

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

1344 """ 

1345 return self._testcases 

1346 

1347 @readonly 

1348 def TestcaseCount(self) -> int: 

1349 """ 

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

1351 

1352 :return: Number of test cases. 

1353 """ 

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

1355 

1356 @readonly 

1357 def AssertionCount(self) -> int: 

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

1359 

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

1361 return self.__class__( 

1362 self._name, 

1363 self._startTime, 

1364 self._setupDuration, 

1365 self._teardownDuration, 

1366 self._totalDuration, 

1367 self._status, 

1368 self._warningCount, 

1369 self._errorCount, 

1370 self._fatalCount 

1371 ) 

1372 

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

1374 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration = super().Aggregate() 

1375 

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

1377 wc, ec, fc, td = testcase.Aggregate(strict) 

1378 

1379 tests += 1 

1380 

1381 warningCount += wc 

1382 errorCount += ec 

1383 fatalCount += fc 

1384 

1385 totalDuration += td 

1386 

1387 status = testcase._status 

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

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

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

1391 inconsistent += 1 

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

1393 excluded += 1 

1394 elif status is TestcaseStatus.Skipped: 

1395 skipped += 1 

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

1397 errored += 1 

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

1399 weak += 1 

1400 elif status is TestcaseStatus.Passed: 

1401 passed += 1 

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

1403 failed += 1 

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

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

1406 else: 

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

1408 

1409 self._tests = tests 

1410 self._inconsistent = inconsistent 

1411 self._excluded = excluded 

1412 self._skipped = skipped 

1413 self._errored = errored 

1414 self._weak = weak 

1415 self._failed = failed 

1416 self._passed = passed 

1417 

1418 self._warningCount = warningCount 

1419 self._errorCount = errorCount 

1420 self._fatalCount = fatalCount 

1421 

1422 if self._totalDuration is None: 

1423 self._totalDuration = totalDuration 

1424 

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

1426 self._status = TestsuiteStatus.Errored 

1427 elif failed > 0: 

1428 self._status = TestsuiteStatus.Failed 

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

1430 self._status = TestsuiteStatus.Empty 

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

1432 self._status = TestsuiteStatus.Passed 

1433 elif tests == skipped: 

1434 self._status = TestsuiteStatus.Skipped 

1435 else: 

1436 self._status = TestsuiteStatus.Unknown 

1437 

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

1439 

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

1441 """ 

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

1443 

1444 :param testcase: The test case to add. 

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

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

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

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

1449 """ 

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

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

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

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

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

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

1456 raise ex 

1457 

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

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

1460 

1461 if testcase._name in self._testcases: 

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

1463 

1464 testcase._parent = self 

1465 self._testcases[testcase._name] = testcase 

1466 

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

1468 """ 

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

1470 

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

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

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

1474 """ 

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

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

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

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

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

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

1481 raise ex 

1482 

1483 for testcase in testcases: 

1484 self.AddTestcase(testcase) 

1485 

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

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

1488 

1489 if IterationScheme.PreOrder in scheme: 

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

1491 yield self 

1492 

1493 if IterationScheme.IncludeTestcases in scheme: 

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

1495 yield testcase 

1496 

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

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

1499 

1500 if IterationScheme.PostOrder in scheme: 

1501 if IterationScheme.IncludeTestcases in scheme: 

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

1503 yield testcase 

1504 

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

1506 yield self 

1507 

1508 def __str__(self) -> str: 

1509 return ( 

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

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

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

1513 ) 

1514 

1515 

1516@export 

1517class TestsuiteSummary(TestsuiteBase[TestsuiteType]): 

1518 """ 

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

1520 

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

1522 """ 

1523 

1524 def __init__( 

1525 self, 

1526 name: str, 

1527 startTime: Nullable[datetime] = None, 

1528 setupDuration: Nullable[timedelta] = None, 

1529 testDuration: Nullable[timedelta] = None, 

1530 teardownDuration: Nullable[timedelta] = None, 

1531 totalDuration: Nullable[timedelta] = None, 

1532 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1533 warningCount: int = 0, 

1534 errorCount: int = 0, 

1535 fatalCount: int = 0, 

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

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

1538 parent: Nullable[TestsuiteType] = None 

1539 ): 

1540 """ 

1541 Initializes the fields of a test summary. 

1542 

1543 :param name: Name of the test summary. 

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

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

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

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

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

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

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

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

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

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

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

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

1556 """ 

1557 super().__init__( 

1558 name, 

1559 TestsuiteKind.Root, 

1560 startTime, 

1561 setupDuration, 

1562 testDuration, 

1563 teardownDuration, 

1564 totalDuration, 

1565 status, 

1566 warningCount, 

1567 errorCount, 

1568 fatalCount, 

1569 testsuites, 

1570 keyValuePairs, 

1571 parent 

1572 ) 

1573 

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

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

1576 

1577 self._tests = tests 

1578 self._inconsistent = inconsistent 

1579 self._excluded = excluded 

1580 self._skipped = skipped 

1581 self._errored = errored 

1582 self._weak = weak 

1583 self._failed = failed 

1584 self._passed = passed 

1585 

1586 self._warningCount = warningCount 

1587 self._errorCount = errorCount 

1588 self._fatalCount = fatalCount 

1589 

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

1591 self._totalDuration = totalDuration 

1592 

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

1594 self._status = TestsuiteStatus.Errored 

1595 elif failed > 0: 

1596 self._status = TestsuiteStatus.Failed 

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

1598 self._status = TestsuiteStatus.Empty 

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

1600 self._status = TestsuiteStatus.Passed 

1601 elif tests == skipped: 

1602 self._status = TestsuiteStatus.Skipped 

1603 elif tests == excluded: 

1604 self._status = TestsuiteStatus.Excluded 

1605 else: 

1606 self._status = TestsuiteStatus.Unknown 

1607 

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

1609 

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

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

1612 yield self 

1613 

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

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

1616 

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

1618 yield self 

1619 

1620 def __str__(self) -> str: 

1621 return ( 

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

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

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

1625 ) 

1626 

1627 

1628@export 

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

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

1631 

1632 _path: Path 

1633 

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

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

1636 

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

1638 self._path = reportFile 

1639 

1640 self._analysisDuration = -1.0 

1641 self._modelConversion = -1.0 

1642 

1643 if analyzeAndConvert: 

1644 self.Analyze() 

1645 self.Convert() 

1646 

1647 @readonly 

1648 def Path(self) -> Path: 

1649 """ 

1650 Read-only property returning the path to the file of this document. 

1651 

1652 :return: The document's path to the file. 

1653 """ 

1654 return self._path 

1655 

1656 @readonly 

1657 def AnalysisDuration(self) -> timedelta: 

1658 """ 

1659 Read-only property returning analysis duration. 

1660 

1661 .. note:: 

1662 

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

1664 content to the test entity hierarchy. 

1665 

1666 :return: Duration to analyze the document. 

1667 """ 

1668 return timedelta(seconds=self._analysisDuration) 

1669 

1670 @readonly 

1671 def ModelConversionDuration(self) -> timedelta: 

1672 """ 

1673 Read-only property returning conversion duration. 

1674 

1675 .. note:: 

1676 

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

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

1679 

1680 :return: Duration to convert the document. 

1681 """ 

1682 return timedelta(seconds=self._modelConversion) 

1683 

1684 @abstractmethod 

1685 def Analyze(self) -> None: 

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

1687 

1688 # @abstractmethod 

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

1690 # pass 

1691 

1692 @abstractmethod 

1693 def Convert(self): 

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

1695 

1696 

1697@export 

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

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

1700 

1701 _mergedCount: int 

1702 

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

1704 self._mergedCount = mergedCount 

1705 

1706 @readonly 

1707 def MergedCount(self) -> int: 

1708 return self._mergedCount 

1709 

1710 

1711@export 

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

1713 _combinedCount: int 

1714 

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

1716 self._combinedCount = combinedCound 

1717 

1718 @readonly 

1719 def CombinedCount(self) -> int: 

1720 return self._combinedCount 

1721 

1722 

1723@export 

1724class MergedTestcase(Testcase, Merged): 

1725 _mergedTestcases: List[Testcase] 

1726 

1727 def __init__( 

1728 self, 

1729 testcase: Testcase, 

1730 parent: Nullable["Testsuite"] = None 

1731 ): 

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

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

1734 

1735 super().__init__( 

1736 testcase._name, 

1737 testcase._startTime, 

1738 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration, 

1739 TestcaseStatus.Unknown, 

1740 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount, 

1741 testcase._warningCount, testcase._errorCount, testcase._fatalCount, 

1742 parent 

1743 ) 

1744 Merged.__init__(self) 

1745 

1746 self._mergedTestcases = [testcase] 

1747 

1748 @readonly 

1749 def Status(self) -> TestcaseStatus: 

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

1751 status = self._mergedTestcases[0]._status 

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

1753 status @= mtc._status 

1754 

1755 self._status = status 

1756 

1757 return self._status 

1758 

1759 @readonly 

1760 def SummedAssertionCount(self) -> int: 

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

1762 

1763 @readonly 

1764 def SummedPassedAssertionCount(self) -> int: 

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

1766 

1767 @readonly 

1768 def SummedFailedAssertionCount(self) -> int: 

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

1770 

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

1772 firstMTC = self._mergedTestcases[0] 

1773 

1774 status = firstMTC._status 

1775 warningCount = firstMTC._warningCount 

1776 errorCount = firstMTC._errorCount 

1777 fatalCount = firstMTC._fatalCount 

1778 totalDuration = firstMTC._totalDuration 

1779 

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

1781 status @= mtc._status 

1782 warningCount += mtc._warningCount 

1783 errorCount += mtc._errorCount 

1784 fatalCount += mtc._fatalCount 

1785 

1786 self._status = status 

1787 

1788 return warningCount, errorCount, fatalCount, totalDuration 

1789 

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

1791 self._mergedCount += 1 

1792 

1793 self._mergedTestcases.append(tc) 

1794 

1795 self._warningCount += tc._warningCount 

1796 self._errorCount += tc._errorCount 

1797 self._fatalCount += tc._fatalCount 

1798 

1799 def ToTestcase(self) -> Testcase: 

1800 return Testcase( 

1801 self._name, 

1802 self._startTime, 

1803 self._setupDuration, 

1804 self._testDuration, 

1805 self._teardownDuration, 

1806 self._totalDuration, 

1807 self._status, 

1808 self._assertionCount, 

1809 self._failedAssertionCount, 

1810 self._passedAssertionCount, 

1811 self._warningCount, 

1812 self._errorCount, 

1813 self._fatalCount 

1814 ) 

1815 

1816 

1817@export 

1818class MergedTestsuite(Testsuite, Merged): 

1819 def __init__( 

1820 self, 

1821 testsuite: Testsuite, 

1822 addTestsuites: bool = False, 

1823 addTestcases: bool = False, 

1824 parent: Nullable["Testsuite"] = None 

1825 ): 

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

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

1828 

1829 super().__init__( 

1830 testsuite._name, 

1831 testsuite._kind, 

1832 testsuite._startTime, 

1833 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration, 

1834 TestsuiteStatus.Unknown, 

1835 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount, 

1836 parent 

1837 ) 

1838 Merged.__init__(self) 

1839 

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

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

1842 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases) 

1843 self.AddTestsuite(mergedTestsuite) 

1844 

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

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

1847 mergedTestcase = MergedTestcase(tc) 

1848 self.AddTestcase(mergedTestcase) 

1849 

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

1851 self._mergedCount += 1 

1852 

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

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

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

1856 else: 

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

1858 self.AddTestsuite(mergedTestsuite) 

1859 

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

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

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

1863 else: 

1864 mergedTestcase = MergedTestcase(tc) 

1865 self.AddTestcase(mergedTestcase) 

1866 

1867 def ToTestsuite(self) -> Testsuite: 

1868 testsuite = Testsuite( 

1869 self._name, 

1870 self._kind, 

1871 self._startTime, 

1872 self._setupDuration, 

1873 self._testDuration, 

1874 self._teardownDuration, 

1875 self._totalDuration, 

1876 self._status, 

1877 self._warningCount, 

1878 self._errorCount, 

1879 self._fatalCount, 

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

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

1882 ) 

1883 

1884 testsuite._tests = self._tests 

1885 testsuite._excluded = self._excluded 

1886 testsuite._inconsistent = self._inconsistent 

1887 testsuite._skipped = self._skipped 

1888 testsuite._errored = self._errored 

1889 testsuite._weak = self._weak 

1890 testsuite._failed = self._failed 

1891 testsuite._passed = self._passed 

1892 

1893 return testsuite 

1894 

1895 

1896@export 

1897class MergedTestsuiteSummary(TestsuiteSummary, Merged): 

1898 _mergedFiles: Dict[Path, TestsuiteSummary] 

1899 

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

1901 super().__init__(name) 

1902 Merged.__init__(self, mergedCount=0) 

1903 

1904 self._mergedFiles = {} 

1905 

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

1907 # if summary.File in self._mergedFiles: 

1908 # raise 

1909 

1910 # FIXME: a summary is not necessarily a file 

1911 self._mergedCount += 1 

1912 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary 

1913 

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

1915 if testsuite._name in self._testsuites: 

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

1917 else: 

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

1919 self.AddTestsuite(mergedTestsuite) 

1920 

1921 def ToTestsuiteSummary(self) -> TestsuiteSummary: 

1922 testsuiteSummary = TestsuiteSummary( 

1923 self._name, 

1924 self._startTime, 

1925 self._setupDuration, 

1926 self._testDuration, 

1927 self._teardownDuration, 

1928 self._totalDuration, 

1929 self._status, 

1930 self._warningCount, 

1931 self._errorCount, 

1932 self._fatalCount, 

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

1934 ) 

1935 

1936 testsuiteSummary._tests = self._tests 

1937 testsuiteSummary._excluded = self._excluded 

1938 testsuiteSummary._inconsistent = self._inconsistent 

1939 testsuiteSummary._skipped = self._skipped 

1940 testsuiteSummary._errored = self._errored 

1941 testsuiteSummary._weak = self._weak 

1942 testsuiteSummary._failed = self._failed 

1943 testsuiteSummary._passed = self._passed 

1944 

1945 return testsuiteSummary