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

843 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-08-29 03:43 +0000

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2024-2026 Electronic Design Automation Abstraction (EDA²) # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

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

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

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

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

36derived from a summary class. 

37 

38**Data Model** 

39 

40.. mermaid:: 

41 

42 graph TD; 

43 doc[Document] 

44 sum[Summary] 

45 ts1[Testsuite] 

46 ts2[Testsuite] 

47 ts21[Testsuite] 

48 tc11[Testcase] 

49 tc12[Testcase] 

50 tc13[Testcase] 

51 tc21[Testcase] 

52 tc22[Testcase] 

53 tc211[Testcase] 

54 tc212[Testcase] 

55 tc213[Testcase] 

56 

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

58 sum --> ts1:::suite 

59 sum --> ts2:::suite 

60 ts2 --> ts21:::suite 

61 ts1 --> tc11:::case 

62 ts1 --> tc12:::case 

63 ts1 --> tc13:::case 

64 ts2 --> tc21:::case 

65 ts2 --> tc22:::case 

66 ts21 --> tc211:::case 

67 ts21 --> tc212:::case 

68 ts21 --> tc213:::case 

69 

70 classDef root fill:#4dc3ff 

71 classDef summary fill:#80d4ff 

72 classDef suite fill:#b3e6ff 

73 classDef case fill:#eeccff 

74""" 

75from datetime import timedelta, datetime 

76from enum import Flag, IntEnum 

77from pathlib import Path 

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

79 

80from pyTooling.Common import getFullyQualifiedName 

81from pyTooling.Decorators import export, readonly 

82from pyTooling.MetaClasses import ExtendedType, abstractmethod 

83from pyTooling.Tree import Node 

84 

85from pyEDAA.Reports import ReportException 

86 

87 

88@export 

89class UnittestException(ReportException): 

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

91 

92 

93@export 

94class AlreadyInHierarchyException(UnittestException): 

95 """ 

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

97 

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

99 hierarchy should occur only once in the hierarchy. 

100 

101 .. hint:: 

102 

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

104 """ 

105 

106 

107@export 

108class DuplicateTestsuiteException(UnittestException): 

109 """ 

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

111 

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

113 

114 .. hint:: 

115 

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

117 """ 

118 

119 

120@export 

121class DuplicateTestcaseException(UnittestException): 

122 """ 

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

124 

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

126 

127 .. hint:: 

128 

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

130 """ 

131 

132 

133@export 

134class TestcaseStatus(Flag): 

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

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

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

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

139 Weak = 4 #: No assertions were recorded. 

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

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

142 

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

144 

145 Inverted = 128 #: To mark inverted results 

146 UnexpectedPassed = Failed | Inverted 

147 ExpectedFailed = Passed | Inverted 

148 

149 Warned = 1024 #: Runtime warning 

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

151 Aborted = 4096 #: Uncaught runtime exception 

152 

153 SetupError = 8192 #: Preparation / compilation error 

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

155 Inconsistent = 32768 #: Dataset is inconsistent 

156 

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

158 

159 # TODO: timed out ? 

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

161 

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

163 s = self & self.Mask 

164 o = other & self.Mask 

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

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

167 elif s is self.Skipped: 

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

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

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

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

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

173 resolved = self.Failed 

174 else: 

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

176 elif s is self.Failed: 

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

178 else: 

179 resolved = self.Unknown 

180 

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

182 return resolved 

183 

184 

185@export 

186class TestsuiteStatus(Flag): 

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

188 Unknown = 0 

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

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

191 Empty = 4 #: No tests in suite 

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

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

194 

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

196 

197 Inverted = 128 #: To mark inverted results 

198 UnexpectedPassed = Failed | Inverted 

199 ExpectedFailed = Passed | Inverted 

200 

201 Warned = 1024 #: Runtime warning 

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

203 Aborted = 4096 #: Uncaught runtime exception 

204 

205 SetupError = 8192 #: Preparation / compilation error 

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

207 

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

209 

210 

211@export 

212class TestsuiteKind(IntEnum): 

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

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

215 Logical = 1 #: Represents a logical unit. 

216 Namespace = 2 #: Represents a namespace. 

217 Package = 3 #: Represents a package. 

218 Module = 4 #: Represents a module. 

219 Class = 5 #: Represents a class. 

220 

221 

222@export 

223class IterationScheme(Flag): 

224 """ 

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

226 

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

228 and what elements to return as a result. 

229 """ 

230 Unknown = 0 #: Neutral element. 

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

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

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

234 

235 Recursive = 8 #: Iterate recursively. 

236 

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

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

239 

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

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

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

243 

244 

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

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

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

248 

249 

250@export 

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

252 """ 

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

254 

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

256 hierarchy. 

257 

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

259 child. |br| 

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

261 

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

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

264 

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

266 hierarchy. 

267 

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

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

270 """ 

271 

272 _parent: Nullable["TestsuiteBase"] 

273 _name: str 

274 

275 _startTime: Nullable[datetime] 

276 _setupDuration: Nullable[timedelta] 

277 _testDuration: Nullable[timedelta] 

278 _teardownDuration: Nullable[timedelta] 

279 _totalDuration: Nullable[timedelta] 

280 

281 _warningCount: int 

282 _errorCount: int 

283 _fatalCount: int 

284 

285 _expectedWarningCount: int 

286 _expectedErrorCount: int 

287 _expectedFatalCount: int 

288 

289 _dict: Dict[str, Any] 

290 

291 def __init__( 

292 self, 

293 name: str, 

294 startTime: Nullable[datetime] = None, 

295 setupDuration: Nullable[timedelta] = None, 

296 testDuration: Nullable[timedelta] = None, 

297 teardownDuration: Nullable[timedelta] = None, 

298 totalDuration: Nullable[timedelta] = None, 

299 warningCount: int = 0, 

300 errorCount: int = 0, 

301 fatalCount: int = 0, 

302 expectedWarningCount: int = 0, 

303 expectedErrorCount: int = 0, 

304 expectedFatalCount: int = 0, 

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

306 parent: Nullable["TestsuiteBase"] = None 

307 ) -> None: 

308 """ 

309 Initializes the fields of the base-class. 

310 

311 :param name: Name of the test entity. 

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

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

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

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

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

317 :param warningCount: Count of encountered warnings. 

318 :param errorCount: Count of encountered errors. 

319 :param fatalCount: Count of encountered fatal errors. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

338 """ 

339 

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

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

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

343 raise ex 

344 

345 if name is None: 

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

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

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

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

350 raise ex 

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

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

353 

354 self._parent = parent 

355 self._name = name 

356 

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

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

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

360 raise ex 

361 

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

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

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

365 raise ex 

366 

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

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

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

370 raise ex 

371 

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

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

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

375 raise ex 

376 

377 if testDuration is not None: 

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

379 if teardownDuration is not None: 

380 if totalDuration is not None: 

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

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

383 else: # no total 

384 totalDuration = setupDuration + testDuration + teardownDuration 

385 # no teardown 

386 elif totalDuration is not None: 

387 if totalDuration < (setupDuration + testDuration): 

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

389 # no teardown, no total 

390 else: 

391 totalDuration = setupDuration + testDuration 

392 # no setup 

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

394 if totalDuration is not None: 

395 if totalDuration < (testDuration + teardownDuration): 

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

397 else: # no setup, no total 

398 totalDuration = testDuration + teardownDuration 

399 # no setup, no teardown 

400 elif totalDuration is not None: 

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

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

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

404 totalDuration = testDuration 

405 # no test 

406 elif totalDuration is not None: 

407 testDuration = totalDuration 

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

409 testDuration -= setupDuration 

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

411 testDuration -= teardownDuration 

412 

413 self._startTime = startTime 

414 self._setupDuration = setupDuration 

415 self._testDuration = testDuration 

416 self._teardownDuration = teardownDuration 

417 self._totalDuration = totalDuration 

418 

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

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

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

422 raise ex 

423 

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

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

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

427 raise ex 

428 

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

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

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

432 raise ex 

433 

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

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

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

437 raise ex 

438 

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

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

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

442 raise ex 

443 

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

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

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

447 raise ex 

448 

449 self._warningCount = warningCount 

450 self._errorCount = errorCount 

451 self._fatalCount = fatalCount 

452 self._expectedWarningCount = expectedWarningCount 

453 self._expectedErrorCount = expectedErrorCount 

454 self._expectedFatalCount = expectedFatalCount 

455 

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

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

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

459 raise ex 

460 

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

462 

463 # QUESTION: allow Parent as setter? 

464 @readonly 

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

466 """ 

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

468 

469 :returns: Reference to the parent entity. 

470 """ 

471 return self._parent 

472 

473 @readonly 

474 def Name(self) -> str: 

475 """ 

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

477 

478 :returns: The test entities name. 

479 """ 

480 return self._name 

481 

482 @readonly 

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

484 """ 

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

486 

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

488 """ 

489 return self._startTime 

490 

491 @readonly 

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

493 """ 

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

495 

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

497 """ 

498 return self._setupDuration 

499 

500 @readonly 

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

502 """ 

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

504 

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

506 distinguishable, assign setup and teardown durations with zero. 

507 

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

509 """ 

510 return self._testDuration 

511 

512 @readonly 

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

514 """ 

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

516 

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

518 """ 

519 return self._teardownDuration 

520 

521 @readonly 

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

523 """ 

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

525 

526 this duration includes setup and teardown durations. 

527 

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

529 """ 

530 return self._totalDuration 

531 

532 @readonly 

533 def WarningCount(self) -> int: 

534 """ 

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

536 

537 :returns: Count of encountered warnings. 

538 """ 

539 return self._warningCount 

540 

541 @readonly 

542 def ErrorCount(self) -> int: 

543 """ 

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

545 

546 :returns: Count of encountered errors. 

547 """ 

548 return self._errorCount 

549 

550 @readonly 

551 def FatalCount(self) -> int: 

552 """ 

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

554 

555 :returns: Count of encountered fatal errors. 

556 """ 

557 return self._fatalCount 

558 

559 @readonly 

560 def ExpectedWarningCount(self) -> int: 

561 """ 

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

563 

564 :returns: Count of expected warnings. 

565 """ 

566 return self._expectedWarningCount 

567 

568 @readonly 

569 def ExpectedErrorCount(self) -> int: 

570 """ 

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

572 

573 :returns: Count of expected errors. 

574 """ 

575 return self._expectedErrorCount 

576 

577 @readonly 

578 def ExpectedFatalCount(self) -> int: 

579 """ 

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

581 

582 :returns: Count of expected fatal errors. 

583 """ 

584 return self._expectedFatalCount 

585 

586 def __len__(self) -> int: 

587 """ 

588 Returns the number of annotated key-value pairs. 

589 

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

591 """ 

592 return len(self._dict) 

593 

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

595 """ 

596 Access a key-value pair by key. 

597 

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

599 :returns: Value of the accessed key. 

600 """ 

601 return self._dict[key] 

602 

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

604 """ 

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

606 

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

608 

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

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

611 """ 

612 self._dict[key] = value 

613 

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

615 """ 

616 Delete a key-value pair by key. 

617 

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

619 """ 

620 del self._dict[key] 

621 

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

623 """ 

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

625 

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

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

628 """ 

629 return key in self._dict 

630 

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

632 """ 

633 Iterate all annotated key-value pairs. 

634 

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

636 """ 

637 yield from self._dict.items() 

638 

639 @abstractmethod 

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

641 """ 

642 Aggregate all test entities in the hierarchy. 

643 """ 

644 

645 @abstractmethod 

646 def __str__(self) -> str: 

647 """ 

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

649 """ 

650 

651 

652@export 

653class Testcase(Base): 

654 """ 

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

656 

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

658 

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

660 

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

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

663 """ 

664 

665 _status: TestcaseStatus 

666 _assertionCount: Nullable[int] 

667 _failedAssertionCount: Nullable[int] 

668 _passedAssertionCount: Nullable[int] 

669 

670 def __init__( 

671 self, 

672 name: str, 

673 startTime: Nullable[datetime] = None, 

674 setupDuration: Nullable[timedelta] = None, 

675 testDuration: Nullable[timedelta] = None, 

676 teardownDuration: Nullable[timedelta] = None, 

677 totalDuration: Nullable[timedelta] = None, 

678 status: TestcaseStatus = TestcaseStatus.Unknown, 

679 assertionCount: Nullable[int] = None, 

680 failedAssertionCount: Nullable[int] = None, 

681 passedAssertionCount: Nullable[int] = None, 

682 warningCount: int = 0, 

683 errorCount: int = 0, 

684 fatalCount: int = 0, 

685 expectedWarningCount: int = 0, 

686 expectedErrorCount: int = 0, 

687 expectedFatalCount: int = 0, 

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

689 parent: Nullable["Testsuite"] = None 

690 ) -> None: 

691 """ 

692 Initializes the fields of a test case. 

693 

694 :param name: Name of the test entity. 

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

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

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

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

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

700 :param status: Status of the test case. 

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

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

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

704 :param warningCount: Count of encountered warnings. 

705 :param errorCount: Count of encountered errors. 

706 :param fatalCount: Count of encountered fatal errors. 

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

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

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

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

711 """ 

712 

713 if parent is not None: 

714 if not isinstance(parent, Testsuite): 

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

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

717 raise ex 

718 

719 parent._testcases[name] = self 

720 

721 super().__init__( 

722 name, 

723 startTime, 

724 setupDuration, testDuration, teardownDuration, totalDuration, 

725 warningCount, errorCount, fatalCount, 

726 expectedWarningCount, expectedErrorCount, expectedFatalCount, 

727 keyValuePairs, 

728 parent=parent 

729 ) 

730 

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

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

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

734 raise ex 

735 

736 self._status = status 

737 

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

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

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

741 raise ex 

742 

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

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

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

746 raise ex 

747 

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

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

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

751 raise ex 

752 

753 self._assertionCount = assertionCount 

754 if assertionCount is not None: 

755 if failedAssertionCount is not None: 

756 self._failedAssertionCount = failedAssertionCount 

757 

758 if passedAssertionCount is not None: 

759 if passedAssertionCount + failedAssertionCount != assertionCount: 

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

761 

762 self._passedAssertionCount = passedAssertionCount 

763 else: 

764 self._passedAssertionCount = assertionCount - failedAssertionCount 

765 elif passedAssertionCount is not None: 

766 self._passedAssertionCount = passedAssertionCount 

767 self._failedAssertionCount = assertionCount - passedAssertionCount 

768 else: 

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

770 elif failedAssertionCount is not None: 

771 self._failedAssertionCount = failedAssertionCount 

772 

773 if passedAssertionCount is not None: 

774 self._passedAssertionCount = passedAssertionCount 

775 self._assertionCount = passedAssertionCount + failedAssertionCount 

776 else: 

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

778 elif passedAssertionCount is not None: 

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

780 else: 

781 self._passedAssertionCount = None 

782 self._failedAssertionCount = None 

783 

784 @readonly 

785 def Status(self) -> TestcaseStatus: 

786 """ 

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

788 

789 :returns: The test case's status. 

790 """ 

791 return self._status 

792 

793 @readonly 

794 def AssertionCount(self) -> int: 

795 """ 

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

797 

798 :returns: Number of assertions. 

799 """ 

800 if self._assertionCount is None: 

801 return 0 

802 return self._assertionCount 

803 

804 @readonly 

805 def FailedAssertionCount(self) -> int: 

806 """ 

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

808 

809 :returns: Number of assertions. 

810 """ 

811 return self._failedAssertionCount 

812 

813 @readonly 

814 def PassedAssertionCount(self) -> int: 

815 """ 

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

817 

818 :returns: Number of passed assertions. 

819 """ 

820 return self._passedAssertionCount 

821 

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

823 return self.__class__( 

824 self._name, 

825 self._startTime, 

826 self._setupDuration, 

827 self._testDuration, 

828 self._teardownDuration, 

829 self._totalDuration, 

830 self._status, 

831 self._warningCount, 

832 self._errorCount, 

833 self._fatalCount, 

834 self._expectedWarningCount, 

835 self._expectedErrorCount, 

836 self._expectedFatalCount, 

837 ) 

838 # TODO: copy key-value-pairs? 

839 

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

841 if self._status is TestcaseStatus.Unknown: 

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

843 self._status = TestcaseStatus.Passed 

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

845 self._status = TestcaseStatus.Weak 

846 elif self._failedAssertionCount == 0: 

847 self._status = TestcaseStatus.Passed 

848 else: 

849 self._status = TestcaseStatus.Failed 

850 

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

852 self._status |= TestcaseStatus.Warned 

853 

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

855 self._status |= TestcaseStatus.Errored 

856 

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

858 self._status |= TestcaseStatus.Aborted 

859 

860 if strict: 

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

862 

863 # TODO: check for setup errors 

864 # TODO: check for teardown errors 

865 

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

867 

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

869 

870 def __str__(self) -> str: 

871 """ 

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

873 

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

875 

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

877 """ 

878 return ( 

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

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

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

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

883 ) 

884 

885 

886@export 

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

888 """ 

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

890 

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

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

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

894 """ 

895 

896 _kind: TestsuiteKind 

897 _status: TestsuiteStatus 

898 _testsuites: Dict[str, TestsuiteType] 

899 

900 _tests: int 

901 _inconsistent: int 

902 _excluded: int 

903 _skipped: int 

904 _errored: int 

905 _weak: int 

906 _failed: int 

907 _passed: int 

908 

909 def __init__( 

910 self, 

911 name: str, 

912 kind: TestsuiteKind = TestsuiteKind.Logical, 

913 startTime: Nullable[datetime] = None, 

914 setupDuration: Nullable[timedelta] = None, 

915 testDuration: Nullable[timedelta] = None, 

916 teardownDuration: Nullable[timedelta] = None, 

917 totalDuration: Nullable[timedelta] = None, 

918 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

919 warningCount: int = 0, 

920 errorCount: int = 0, 

921 fatalCount: int = 0, 

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

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

924 parent: Nullable["Testsuite"] = None 

925 ) -> None: 

926 """ 

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

928 

929 :param name: Name of the test entity. 

930 :param kind: Kind of the test entity. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

948 """ 

949 if parent is not None: 

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

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

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

953 raise ex 

954 

955 parent._testsuites[name] = self 

956 

957 super().__init__( 

958 name, 

959 startTime, 

960 setupDuration, 

961 testDuration, 

962 teardownDuration, 

963 totalDuration, 

964 warningCount, 

965 errorCount, 

966 fatalCount, 

967 0, 0, 0, 

968 keyValuePairs, 

969 parent=parent 

970 ) 

971 

972 self._kind = kind 

973 self._status = status 

974 

975 self._testsuites = {} 

976 if testsuites is not None: 

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

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

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

980 raise ex 

981 

982 for testsuite in testsuites: 

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

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

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

986 raise ex 

987 

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

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

990 

991 if testsuite._name in self._testsuites: 

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

993 

994 testsuite._parent = self 

995 self._testsuites[testsuite._name] = testsuite 

996 

997 self._status = TestsuiteStatus.Unknown 

998 self._tests = 0 

999 self._inconsistent = 0 

1000 self._excluded = 0 

1001 self._skipped = 0 

1002 self._errored = 0 

1003 self._weak = 0 

1004 self._failed = 0 

1005 self._passed = 0 

1006 

1007 @readonly 

1008 def Kind(self) -> TestsuiteKind: 

1009 """ 

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

1011 

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

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

1014 language construct. 

1015 

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

1017 

1018 :returns: Kind of the test suite. 

1019 """ 

1020 return self._kind 

1021 

1022 @readonly 

1023 def Status(self) -> TestsuiteStatus: 

1024 """ 

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

1026 

1027 :returns: Overall status of the test suite. 

1028 """ 

1029 return self._status 

1030 

1031 @readonly 

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

1033 """ 

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

1035 

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

1037 """ 

1038 return self._testsuites 

1039 

1040 @readonly 

1041 def TestsuiteCount(self) -> int: 

1042 """ 

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

1044 

1045 :returns: Number of test suites. 

1046 """ 

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

1048 

1049 @readonly 

1050 def TestcaseCount(self) -> int: 

1051 """ 

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

1053 

1054 :returns: Number of test cases. 

1055 """ 

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

1057 

1058 @readonly 

1059 def AssertionCount(self) -> int: 

1060 """ 

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

1062 

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

1064 """ 

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

1066 

1067 @readonly 

1068 def FailedAssertionCount(self) -> int: 

1069 """ 

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

1071 

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

1073 """ 

1074 raise NotImplementedError() 

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

1076 

1077 @readonly 

1078 def PassedAssertionCount(self) -> int: 

1079 """ 

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

1081 

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

1083 """ 

1084 raise NotImplementedError() 

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

1086 

1087 @readonly 

1088 def Tests(self) -> int: 

1089 """ 

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

1091 

1092 :returns: Number of tests. 

1093 """ 

1094 return self._tests 

1095 

1096 @readonly 

1097 def Inconsistent(self) -> int: 

1098 """ 

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

1100 

1101 :returns: Number of inconsistent tests. 

1102 """ 

1103 return self._inconsistent 

1104 

1105 @readonly 

1106 def Excluded(self) -> int: 

1107 """ 

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

1109 

1110 :returns: Number of excluded tests. 

1111 """ 

1112 return self._excluded 

1113 

1114 @readonly 

1115 def Skipped(self) -> int: 

1116 """ 

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

1118 

1119 :returns: Number of skipped tests. 

1120 """ 

1121 return self._skipped 

1122 

1123 @readonly 

1124 def Errored(self) -> int: 

1125 """ 

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

1127 

1128 :returns: Number of errored tests. 

1129 """ 

1130 return self._errored 

1131 

1132 @readonly 

1133 def Weak(self) -> int: 

1134 """ 

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

1136 

1137 :returns: Number of weak tests. 

1138 """ 

1139 return self._weak 

1140 

1141 @readonly 

1142 def Failed(self) -> int: 

1143 """ 

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

1145 

1146 :returns: Number of failed tests. 

1147 """ 

1148 return self._failed 

1149 

1150 @readonly 

1151 def Passed(self) -> int: 

1152 """ 

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

1154 

1155 :returns: Number of passed tests. 

1156 """ 

1157 return self._passed 

1158 

1159 @readonly 

1160 def WarningCount(self) -> int: 

1161 """ 

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

1163 

1164 :returns: Number of warnings. 

1165 """ 

1166 raise NotImplementedError() 

1167 # return self._warningCount 

1168 

1169 @readonly 

1170 def ErrorCount(self) -> int: 

1171 """ 

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

1173 

1174 :returns: Number of errors. 

1175 """ 

1176 raise NotImplementedError() 

1177 # return self._errorCount 

1178 

1179 @readonly 

1180 def FatalCount(self) -> int: 

1181 """ 

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

1183 

1184 :returns: Number of fatal errors. 

1185 """ 

1186 raise NotImplementedError() 

1187 # return self._fatalCount 

1188 

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

1190 tests = 0 

1191 inconsistent = 0 

1192 excluded = 0 

1193 skipped = 0 

1194 errored = 0 

1195 weak = 0 

1196 failed = 0 

1197 passed = 0 

1198 

1199 warningCount = 0 

1200 errorCount = 0 

1201 fatalCount = 0 

1202 

1203 expectedWarningCount = 0 

1204 expectedErrorCount = 0 

1205 expectedFatalCount = 0 

1206 

1207 totalDuration = timedelta() 

1208 

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

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

1211 tests += t 

1212 inconsistent += i 

1213 excluded += ex 

1214 skipped += s 

1215 errored += e 

1216 weak += w 

1217 failed += f 

1218 passed += p 

1219 

1220 warningCount += wc 

1221 errorCount += ec 

1222 fatalCount += fc 

1223 

1224 expectedWarningCount += ewc 

1225 expectedErrorCount += eec 

1226 expectedFatalCount += efc 

1227 

1228 totalDuration += td 

1229 

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

1231 

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

1233 """ 

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

1235 

1236 :param testsuite: The test suite to add. 

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

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

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

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

1241 """ 

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

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

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

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

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

1247 raise ex 

1248 

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

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

1251 

1252 if testsuite._name in self._testsuites: 

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

1254 

1255 testsuite._parent = self 

1256 self._testsuites[testsuite._name] = testsuite 

1257 

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

1259 """ 

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

1261 

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

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

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

1265 """ 

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

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

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

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

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

1271 raise ex 

1272 

1273 for testsuite in testsuites: 

1274 self.AddTestsuite(testsuite) 

1275 

1276 @abstractmethod 

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

1278 pass 

1279 

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

1281 return self.Iterate(scheme) 

1282 

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

1284 return self.Iterate(scheme) 

1285 

1286 def ToTree(self) -> Node: 

1287 rootNode = Node(value=self._name) 

1288 

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

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

1291 

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

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

1294 

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

1296 convertTestsuite(ts, testsuiteNode) 

1297 

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

1299 convertTestcase(tc, testsuiteNode) 

1300 

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

1302 convertTestsuite(testsuite, rootNode) 

1303 

1304 return rootNode 

1305 

1306 

1307@export 

1308class Testsuite(TestsuiteBase[TestsuiteType]): 

1309 """ 

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

1311 

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

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

1314 """ 

1315 

1316 _testcases: Dict[str, "Testcase"] 

1317 _hostname: Nullable[str] 

1318 

1319 def __init__( 

1320 self, 

1321 name: str, 

1322 kind: TestsuiteKind = TestsuiteKind.Logical, 

1323 hostname: Nullable[str] = None, 

1324 startTime: Nullable[datetime] = None, 

1325 setupDuration: Nullable[timedelta] = None, 

1326 testDuration: Nullable[timedelta] = None, 

1327 teardownDuration: Nullable[timedelta] = None, 

1328 totalDuration: Nullable[timedelta] = None, 

1329 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1330 warningCount: int = 0, 

1331 errorCount: int = 0, 

1332 fatalCount: int = 0, 

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

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

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

1336 parent: Nullable[TestsuiteType] = None 

1337 ) -> None: 

1338 """ 

1339 Initializes the fields of a test suite. 

1340 

1341 :param name: Name of the test suite. 

1342 :param kind: Kind of the test suite. 

1343 :param hostname: Name of the host the test suite was executed on, or ``None`` if it wasn't recorded. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1361 """ 

1362 super().__init__( 

1363 name, 

1364 kind, 

1365 startTime, 

1366 setupDuration, 

1367 testDuration, 

1368 teardownDuration, 

1369 totalDuration, 

1370 status, 

1371 warningCount, 

1372 errorCount, 

1373 fatalCount, 

1374 testsuites, 

1375 keyValuePairs, 

1376 parent=parent 

1377 ) 

1378 

1379 # self._testDuration = testDuration 

1380 self._hostname = hostname 

1381 

1382 self._testcases = {} 

1383 if testcases is not None: 

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

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

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

1387 raise ex 

1388 

1389 for testcase in testcases: 

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

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

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

1393 raise ex 

1394 

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

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

1397 

1398 if testcase._name in self._testcases: 

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

1400 

1401 testcase._parent = self 

1402 self._testcases[testcase._name] = testcase 

1403 

1404 @readonly 

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

1406 """ 

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

1408 

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

1410 """ 

1411 return self._testcases 

1412 

1413 @readonly 

1414 def TestcaseCount(self) -> int: 

1415 """ 

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

1417 

1418 :returns: Number of test cases. 

1419 """ 

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

1421 

1422 @readonly 

1423 def AssertionCount(self) -> int: 

1424 """ 

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

1426 

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

1428 """ 

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

1430 

1431 @readonly 

1432 def Hostname(self) -> Nullable[str]: 

1433 """ 

1434 Read-only property to access the name of the host this test suite was executed on (:attr:`_hostname`). 

1435 

1436 :returns: The hostname, or ``None`` if it wasn't recorded. 

1437 """ 

1438 return self._hostname 

1439 

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

1441 return self.__class__( 

1442 self._name, 

1443 self._kind, 

1444 self._hostname, 

1445 self._startTime, 

1446 self._setupDuration, 

1447 self._testDuration, 

1448 self._teardownDuration, 

1449 self._totalDuration, 

1450 self._status, 

1451 self._warningCount, 

1452 self._errorCount, 

1453 self._fatalCount 

1454 ) 

1455 

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

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

1458 

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

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

1461 

1462 tests += 1 

1463 

1464 warningCount += wc 

1465 errorCount += ec 

1466 fatalCount += fc 

1467 

1468 expectedWarningCount += ewc 

1469 expectedErrorCount += eec 

1470 expectedFatalCount += efc 

1471 

1472 totalDuration += td 

1473 

1474 status = testcase._status 

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

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

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

1478 inconsistent += 1 

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

1480 excluded += 1 

1481 elif status is TestcaseStatus.Skipped: 

1482 skipped += 1 

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

1484 errored += 1 

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

1486 weak += 1 

1487 elif status is TestcaseStatus.Passed: 

1488 passed += 1 

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

1490 failed += 1 

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

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

1493 else: 

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

1495 

1496 self._tests = tests 

1497 self._inconsistent = inconsistent 

1498 self._excluded = excluded 

1499 self._skipped = skipped 

1500 self._errored = errored 

1501 self._weak = weak 

1502 self._failed = failed 

1503 self._passed = passed 

1504 

1505 self._warningCount = warningCount 

1506 self._errorCount = errorCount 

1507 self._fatalCount = fatalCount 

1508 

1509 self._expectedWarningCount = expectedWarningCount 

1510 self._expectedErrorCount = expectedErrorCount 

1511 self._expectedFatalCount = expectedFatalCount 

1512 

1513 if self._totalDuration is None: 

1514 self._totalDuration = totalDuration 

1515 

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

1517 self._status = TestsuiteStatus.Errored 

1518 elif failed > 0: 

1519 self._status = TestsuiteStatus.Failed 

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

1521 self._status = TestsuiteStatus.Empty 

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

1523 self._status = TestsuiteStatus.Passed 

1524 elif tests == skipped: 

1525 self._status = TestsuiteStatus.Skipped 

1526 else: 

1527 self._status = TestsuiteStatus.Unknown 

1528 

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

1530 

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

1532 """ 

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

1534 

1535 :param testcase: The test case to add. 

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

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

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

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

1540 """ 

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

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

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

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

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

1546 raise ex 

1547 

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

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

1550 

1551 if testcase._name in self._testcases: 

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

1553 

1554 testcase._parent = self 

1555 self._testcases[testcase._name] = testcase 

1556 

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

1558 """ 

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

1560 

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

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

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

1564 """ 

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

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

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

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

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

1570 raise ex 

1571 

1572 for testcase in testcases: 

1573 self.AddTestcase(testcase) 

1574 

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

1576 if IterationScheme.PreOrder in scheme: 

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

1578 yield self 

1579 

1580 if IterationScheme.IncludeTestcases in scheme: 

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

1582 yield testcase 

1583 

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

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

1586 

1587 if IterationScheme.PostOrder in scheme: 

1588 if IterationScheme.IncludeTestcases in scheme: 

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

1590 yield testcase 

1591 

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

1593 yield self 

1594 

1595 def __str__(self) -> str: 

1596 return ( 

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

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

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

1600 ) 

1601 

1602 

1603@export 

1604class TestsuiteSummary(TestsuiteBase[TestsuiteType]): 

1605 """ 

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

1607 

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

1609 """ 

1610 

1611 def __init__( 

1612 self, 

1613 name: str, 

1614 startTime: Nullable[datetime] = None, 

1615 setupDuration: Nullable[timedelta] = None, 

1616 testDuration: Nullable[timedelta] = None, 

1617 teardownDuration: Nullable[timedelta] = None, 

1618 totalDuration: Nullable[timedelta] = None, 

1619 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1620 warningCount: int = 0, 

1621 errorCount: int = 0, 

1622 fatalCount: int = 0, 

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

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

1625 parent: Nullable[TestsuiteType] = None 

1626 ) -> None: 

1627 """ 

1628 Initializes the fields of a test summary. 

1629 

1630 :param name: Name of the test summary. 

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

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

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

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

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

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

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

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

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

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

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

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

1643 """ 

1644 super().__init__( 

1645 name, 

1646 TestsuiteKind.Root, 

1647 startTime, setupDuration, testDuration, teardownDuration, totalDuration, 

1648 status, 

1649 warningCount, errorCount, fatalCount, 

1650 testsuites, 

1651 keyValuePairs, 

1652 parent=parent 

1653 ) 

1654 

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

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

1657 

1658 self._tests = tests 

1659 self._inconsistent = inconsistent 

1660 self._excluded = excluded 

1661 self._skipped = skipped 

1662 self._errored = errored 

1663 self._weak = weak 

1664 self._failed = failed 

1665 self._passed = passed 

1666 

1667 self._warningCount = warningCount 

1668 self._errorCount = errorCount 

1669 self._fatalCount = fatalCount 

1670 

1671 self._expectedWarningCount = expectedWarningCount 

1672 self._expectedErrorCount = expectedErrorCount 

1673 self._expectedFatalCount = expectedFatalCount 

1674 

1675 if self._totalDuration is None: 

1676 self._totalDuration = totalDuration 

1677 

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

1679 self._status = TestsuiteStatus.Errored 

1680 elif failed > 0: 

1681 self._status = TestsuiteStatus.Failed 

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

1683 self._status = TestsuiteStatus.Empty 

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

1685 self._status = TestsuiteStatus.Passed 

1686 elif tests == skipped: 

1687 self._status = TestsuiteStatus.Skipped 

1688 elif tests == excluded: 

1689 self._status = TestsuiteStatus.Excluded 

1690 else: 

1691 self._status = TestsuiteStatus.Unknown 

1692 

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

1694 

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

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

1697 yield self 

1698 

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

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

1701 

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

1703 yield self 

1704 

1705 def __str__(self) -> str: 

1706 return ( 

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

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

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

1710 ) 

1711 

1712 

1713@export 

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

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

1716 

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

1718 

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

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

1721 

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

1723 self._path = reportFile 

1724 

1725 self._analysisDuration = -1.0 

1726 self._modelConversion = -1.0 

1727 

1728 if analyzeAndConvert: 

1729 self.Analyze() 

1730 self.Convert() 

1731 

1732 @readonly 

1733 def Path(self) -> Path: 

1734 """ 

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

1736 

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

1738 """ 

1739 return self._path 

1740 

1741 @readonly 

1742 def AnalysisDuration(self) -> timedelta: 

1743 """ 

1744 Read-only property to return analysis duration. 

1745 

1746 .. note:: 

1747 

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

1749 content to the test entity hierarchy. 

1750 

1751 :returns: Duration to analyze the document. 

1752 """ 

1753 return timedelta(seconds=self._analysisDuration) 

1754 

1755 @readonly 

1756 def ModelConversionDuration(self) -> timedelta: 

1757 """ 

1758 Read-only property to return conversion duration. 

1759 

1760 .. note:: 

1761 

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

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

1764 

1765 :returns: Duration to convert the document. 

1766 """ 

1767 return timedelta(seconds=self._modelConversion) 

1768 

1769 @abstractmethod 

1770 def Analyze(self) -> None: 

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

1772 

1773 # @abstractmethod 

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

1775 # pass 

1776 

1777 @abstractmethod 

1778 def Convert(self): 

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

1780 

1781 

1782@export 

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

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

1785 

1786 _mergedCount: int 

1787 

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

1789 self._mergedCount = mergedCount 

1790 

1791 def _MergeStartTime(self, otherStartTime: Nullable[datetime]) -> Nullable[datetime]: 

1792 """ 

1793 Combine this entity's start time with the start time of an entity being merged in. 

1794 

1795 A merged entity started when the earliest of its parts started. An entity without a start time contributes 

1796 nothing rather than erasing what is known. 

1797 

1798 :param otherStartTime: The start time of the entity being merged in. 

1799 :returns: The earlier of the two start times, or ``None`` if neither has one. 

1800 """ 

1801 if otherStartTime is None: 

1802 return self._startTime 

1803 elif self._startTime is None or otherStartTime < self._startTime: 

1804 return otherStartTime 

1805 else: 

1806 return self._startTime 

1807 

1808 @readonly 

1809 def MergedCount(self) -> int: 

1810 """ 

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

1812 

1813 :returns: Number of merged entities. 

1814 """ 

1815 return self._mergedCount 

1816 

1817 

1818@export 

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

1820 _combinedCount: int 

1821 

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

1823 self._combinedCount = combinedCound 

1824 

1825 @readonly 

1826 def CombinedCount(self) -> int: 

1827 """ 

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

1829 

1830 :returns: Number of combined entities. 

1831 """ 

1832 return self._combinedCount 

1833 

1834 

1835@export 

1836class MergedTestcase(Testcase, Merged): 

1837 _mergedTestcases: List[Testcase] 

1838 

1839 def __init__( 

1840 self, 

1841 testcase: Testcase, 

1842 parent: Nullable["Testsuite"] = None 

1843 ) -> None: 

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

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

1846 

1847 super().__init__( 

1848 testcase._name, 

1849 testcase._startTime, 

1850 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration, 

1851 TestcaseStatus.Unknown, 

1852 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount, 

1853 testcase._warningCount, testcase._errorCount, testcase._fatalCount, 

1854 testcase._expectedWarningCount, testcase._expectedErrorCount, testcase._expectedFatalCount, 

1855 parent=parent 

1856 ) 

1857 Merged.__init__(self) 

1858 

1859 self._mergedTestcases = [testcase] 

1860 

1861 @readonly 

1862 def Status(self) -> TestcaseStatus: 

1863 """ 

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

1865 

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

1867 """ 

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

1869 status = self._mergedTestcases[0]._status 

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

1871 status @= mtc._status 

1872 

1873 self._status = status 

1874 

1875 return self._status 

1876 

1877 @readonly 

1878 def SummedAssertionCount(self) -> int: 

1879 """ 

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

1881 

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

1883 """ 

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

1885 

1886 @readonly 

1887 def SummedPassedAssertionCount(self) -> int: 

1888 """ 

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

1890 

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

1892 """ 

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

1894 

1895 @readonly 

1896 def SummedFailedAssertionCount(self) -> int: 

1897 """ 

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

1899 

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

1901 """ 

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

1903 

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

1905 firstMTC = self._mergedTestcases[0] 

1906 

1907 status = firstMTC._status 

1908 warningCount = firstMTC._warningCount 

1909 errorCount = firstMTC._errorCount 

1910 fatalCount = firstMTC._fatalCount 

1911 totalDuration = firstMTC._totalDuration 

1912 

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

1914 status @= mtc._status 

1915 warningCount += mtc._warningCount 

1916 errorCount += mtc._errorCount 

1917 fatalCount += mtc._fatalCount 

1918 

1919 self._status = status 

1920 

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

1922 

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

1924 self._mergedCount += 1 

1925 

1926 self._mergedTestcases.append(tc) 

1927 

1928 self._warningCount += tc._warningCount 

1929 self._errorCount += tc._errorCount 

1930 self._fatalCount += tc._fatalCount 

1931 

1932 def ToTestcase(self) -> Testcase: 

1933 return Testcase( 

1934 self._name, 

1935 self._startTime, 

1936 self._setupDuration, 

1937 self._testDuration, 

1938 self._teardownDuration, 

1939 self._totalDuration, 

1940 self._status, 

1941 self._assertionCount, 

1942 self._failedAssertionCount, 

1943 self._passedAssertionCount, 

1944 self._warningCount, 

1945 self._errorCount, 

1946 self._fatalCount 

1947 ) 

1948 

1949 

1950@export 

1951class MergedTestsuite(Testsuite, Merged): 

1952 def __init__( 

1953 self, 

1954 testsuite: Testsuite, 

1955 addTestsuites: bool = False, 

1956 addTestcases: bool = False, 

1957 parent: Nullable["Testsuite"] = None 

1958 ) -> None: 

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

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

1961 

1962 super().__init__( 

1963 testsuite._name, 

1964 testsuite._kind, 

1965 testsuite._hostname, 

1966 testsuite._startTime, 

1967 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration, 

1968 TestsuiteStatus.Unknown, 

1969 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount, 

1970 parent=parent 

1971 ) 

1972 Merged.__init__(self) 

1973 

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

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

1976 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases) 

1977 self.AddTestsuite(mergedTestsuite) 

1978 

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

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

1981 mergedTestcase = MergedTestcase(tc) 

1982 self.AddTestcase(mergedTestcase) 

1983 

1984 def _MergeHostname(self, otherHostname: Nullable[str]) -> Nullable[str]: 

1985 """ 

1986 Combine this test suite's hostname with the hostname of a test suite being merged in. 

1987 

1988 A test suite without a hostname ran somewhere unrecorded, not somewhere else, so it keeps an otherwise 

1989 unanimous hostname. 

1990 

1991 :param otherHostname: The hostname of the test suite being merged in. 

1992 :returns: The common hostname, ``"various"`` if they disagree, or ``None`` if neither has one. 

1993 """ 

1994 if otherHostname is None: 

1995 return self._hostname 

1996 elif self._hostname is None or self._hostname == otherHostname: 

1997 return otherHostname 

1998 else: 

1999 return "various" 

2000 

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

2002 self._mergedCount += 1 

2003 self._hostname = self._MergeHostname(testsuite._hostname) 

2004 self._startTime = self._MergeStartTime(testsuite._startTime) 

2005 

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

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

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

2009 else: 

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

2011 self.AddTestsuite(mergedTestsuite) 

2012 

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

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

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

2016 else: 

2017 mergedTestcase = MergedTestcase(tc) 

2018 self.AddTestcase(mergedTestcase) 

2019 

2020 def ToTestsuite(self) -> Testsuite: 

2021 testsuite = Testsuite( 

2022 self._name, 

2023 self._kind, 

2024 self._hostname, 

2025 self._startTime, 

2026 self._setupDuration, 

2027 self._testDuration, 

2028 self._teardownDuration, 

2029 self._totalDuration, 

2030 self._status, 

2031 self._warningCount, 

2032 self._errorCount, 

2033 self._fatalCount, 

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

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

2036 ) 

2037 

2038 testsuite._tests = self._tests 

2039 testsuite._excluded = self._excluded 

2040 testsuite._inconsistent = self._inconsistent 

2041 testsuite._skipped = self._skipped 

2042 testsuite._errored = self._errored 

2043 testsuite._weak = self._weak 

2044 testsuite._failed = self._failed 

2045 testsuite._passed = self._passed 

2046 

2047 return testsuite 

2048 

2049 

2050@export 

2051class MergedTestsuiteSummary(TestsuiteSummary, Merged): 

2052 _mergedFiles: Dict[Path, TestsuiteSummary] 

2053 

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

2055 super().__init__(name) 

2056 Merged.__init__(self, mergedCount=0) 

2057 

2058 self._mergedFiles = {} 

2059 

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

2061 # if summary.File in self._mergedFiles: 

2062 # raise 

2063 

2064 # FIXME: a summary is not necessarily a file 

2065 self._mergedCount += 1 

2066 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary 

2067 self._startTime = self._MergeStartTime(testsuiteSummary._startTime) 

2068 

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

2070 if testsuite._name in self._testsuites: 

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

2072 else: 

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

2074 self.AddTestsuite(mergedTestsuite) 

2075 

2076 def ToTestsuiteSummary(self) -> TestsuiteSummary: 

2077 testsuiteSummary = TestsuiteSummary( 

2078 self._name, 

2079 self._startTime, 

2080 self._setupDuration, 

2081 self._testDuration, 

2082 self._teardownDuration, 

2083 self._totalDuration, 

2084 self._status, 

2085 self._warningCount, 

2086 self._errorCount, 

2087 self._fatalCount, 

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

2089 ) 

2090 

2091 testsuiteSummary._tests = self._tests 

2092 testsuiteSummary._excluded = self._excluded 

2093 testsuiteSummary._inconsistent = self._inconsistent 

2094 testsuiteSummary._skipped = self._skipped 

2095 testsuiteSummary._errored = self._errored 

2096 testsuiteSummary._weak = self._weak 

2097 testsuiteSummary._failed = self._failed 

2098 testsuiteSummary._passed = self._passed 

2099 

2100 return testsuiteSummary