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

920 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-14 02:26 +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 __future__ import annotations 

76 

77from datetime import timedelta, datetime 

78from enum import Flag, IntEnum 

79from pathlib import Path 

80from typing import Optional as Nullable, Dict, Iterable, Any, Tuple, Generator, Union, List, Generic, \ 

81 TypeVar, Mapping 

82 

83from pyTooling.Common import getFullyQualifiedName 

84from pyTooling.Decorators import export, readonly 

85from pyTooling.Exceptions import AlreadyInHierarchyError, DuplicateElementError 

86from pyTooling.MetaClasses import ExtendedType, abstractmethod 

87from pyTooling.Tree import Node 

88 

89from pyEDAA.Reports import ReportException 

90 

91 

92@export 

93class UnittestError(ReportException): 

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

95 

96 

97@export 

98class AlreadyInTestsuiteError(UnittestError, AlreadyInHierarchyError): 

99 """ 

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

101 

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

103 hierarchy should occur only once in the hierarchy. 

104 

105 .. hint:: 

106 

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

108 """ 

109 

110 

111@export 

112class DuplicateTestsuiteError(UnittestError, DuplicateElementError): 

113 """ 

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

115 

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

117 

118 .. hint:: 

119 

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

121 """ 

122 

123 

124@export 

125class DuplicateTestcaseError(UnittestError, DuplicateElementError): 

126 """ 

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

128 

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

130 

131 .. hint:: 

132 

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

134 """ 

135 

136 

137@export 

138class TestcaseStatus(Flag): 

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

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

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

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

143 Weak = 4 #: No assertions were recorded. 

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

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

146 

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

148 

149 Inverted = 128 #: To mark inverted results 

150 UnexpectedPassed = Failed | Inverted 

151 ExpectedFailed = Passed | Inverted 

152 

153 Warned = 1024 #: Runtime warning 

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

155 Aborted = 4096 #: Uncaught runtime exception 

156 

157 SetupError = 8192 #: Preparation / compilation error 

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

159 Inconsistent = 32768 #: Dataset is inconsistent 

160 

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

162 

163 # TODO: timed out ? 

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

165 

166 def __matmul__(self, other: TestcaseStatus) -> TestcaseStatus: 

167 s = self & self.Mask 

168 o = other & self.Mask 

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

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

171 elif s is self.Skipped: 

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

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

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

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

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

177 resolved = self.Failed 

178 else: 

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

180 elif s is self.Failed: 

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

182 else: 

183 resolved = self.Unknown 

184 

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

186 return resolved 

187 

188 

189@export 

190class TestsuiteStatus(Flag): 

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

192 Unknown = 0 

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

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

195 Empty = 4 #: No tests in suite 

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

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

198 

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

200 

201 Inverted = 128 #: To mark inverted results 

202 UnexpectedPassed = Failed | Inverted 

203 ExpectedFailed = Passed | Inverted 

204 

205 Warned = 1024 #: Runtime warning 

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

207 Aborted = 4096 #: Uncaught runtime exception 

208 

209 SetupError = 8192 #: Preparation / compilation error 

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

211 

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

213 

214 

215@export 

216class TestsuiteKind(IntEnum): 

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

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

219 Logical = 1 #: Represents a logical unit. 

220 Namespace = 2 #: Represents a namespace. 

221 Package = 3 #: Represents a package. 

222 Module = 4 #: Represents a module. 

223 Class = 5 #: Represents a class. 

224 

225 

226@export 

227class IterationScheme(Flag): 

228 """ 

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

230 

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

232 and what elements to return as a result. 

233 """ 

234 Unknown = 0 #: Neutral element. 

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

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

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

238 

239 Recursive = 8 #: Iterate recursively. 

240 

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

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

243 

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

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

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

247 

248 

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

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

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

252 

253 

254@export 

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

256 """ 

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

258 

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

260 hierarchy. 

261 

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

263 child. |br| 

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

265 

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

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

268 

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

270 hierarchy. 

271 

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

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

274 

275 Besides its name, a test entity can carry a title, a summary and a description written for a reader, like the 

276 doc-string of a test method. Formats like JUnit XML can't express these, so they are ``None`` then. 

277 """ 

278 

279 _parent: Nullable[TestsuiteBase] 

280 _name: str 

281 _title: Nullable[str] #: Short label of the test entity, written for a reader. 

282 _summary: Nullable[str] #: Summary of the test entity, e.g. the first paragraph of its doc-string. 

283 _description: Nullable[str] #: Description of the test entity, e.g. its doc-string. 

284 

285 _startTime: Nullable[datetime] 

286 _setupDuration: Nullable[timedelta] 

287 _testDuration: Nullable[timedelta] 

288 _teardownDuration: Nullable[timedelta] 

289 _totalDuration: Nullable[timedelta] 

290 

291 _warningCount: int 

292 _errorCount: int 

293 _fatalCount: int 

294 

295 _expectedWarningCount: int 

296 _expectedErrorCount: int 

297 _expectedFatalCount: int 

298 

299 _dict: Dict[str, Any] 

300 

301 def __init__( 

302 self, 

303 name: str, 

304 title: Nullable[str] = None, 

305 summary: Nullable[str] = None, 

306 description: Nullable[str] = None, 

307 startTime: Nullable[datetime] = None, 

308 setupDuration: Nullable[timedelta] = None, 

309 testDuration: Nullable[timedelta] = None, 

310 teardownDuration: Nullable[timedelta] = None, 

311 totalDuration: Nullable[timedelta] = None, 

312 warningCount: int = 0, 

313 errorCount: int = 0, 

314 fatalCount: int = 0, 

315 expectedWarningCount: int = 0, 

316 expectedErrorCount: int = 0, 

317 expectedFatalCount: int = 0, 

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

319 *, 

320 parent: Nullable[TestsuiteBase] = None 

321 ) -> None: 

322 """ 

323 Initializes the fields of the base-class. 

324 

325 :param name: Name of the test entity. 

326 :param title: Optional, short label of the test entity, written for a reader. 

327 :param summary: Optional, summary of the test entity. 

328 :param description: Optional, description of the test entity. 

329 :param startTime: Optional, time when the test entity was started. 

330 :param setupDuration: Optional, duration it took to set up the entity. 

331 :param testDuration: Optional, duration of the entity's test run. 

332 :param teardownDuration: Optional, duration it took to tear down the entity. 

333 :param totalDuration: Optional, total duration of the entity's execution (setup + test + teardown). 

334 :param warningCount: Optional, count of encountered warnings. 

335 :param errorCount: Optional, count of encountered errors. 

336 :param fatalCount: Optional, count of encountered fatal errors. 

337 :param keyValuePairs: Optional, mapping of key-value pairs to initialize the test entity with. 

338 :param parent: Optional, reference to the parent test entity. 

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

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

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

342 :raises TypeError: When parameter 'title' is not a string. 

343 :raises TypeError: When parameter 'summary' is not a string. 

344 :raises TypeError: When parameter 'description' is not a string. 

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

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

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

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

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

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

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

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

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

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

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

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

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

358 """ 

359 

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

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

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

363 raise ex 

364 

365 if name is None: 

366 raise ValueError("Parameter 'name' is None.") 

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

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

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

370 raise ex 

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

372 raise ValueError("Parameter 'name' is empty.") 

373 

374 self._parent = parent 

375 self._name = name 

376 

377 for parameterName, value in ( 

378 ("testDuration", testDuration), 

379 ("setupDuration", setupDuration), 

380 ("teardownDuration", teardownDuration), 

381 ("totalDuration", totalDuration) 

382 ): 

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

384 ex = TypeError(f"Parameter '{parameterName}' is not of type 'timedelta'.") 

385 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

386 raise ex 

387 

388 if testDuration is not None: 

389 if setupDuration is not None: 

390 if 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 < (setupDuration + testDuration + teardownDuration): 

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

394 else: # no total 

395 totalDuration = setupDuration + testDuration + teardownDuration 

396 # no teardown 

397 elif totalDuration is not None: 397 ↛ 398line 397 didn't jump to line 398 because the condition on line 397 was never true

398 if totalDuration < (setupDuration + testDuration): 

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

400 # no teardown, no total 

401 else: 

402 totalDuration = setupDuration + testDuration 

403 # no setup 

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

405 if totalDuration is not None: 

406 if totalDuration < (testDuration + teardownDuration): 

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

408 else: # no setup, no total 

409 totalDuration = testDuration + teardownDuration 

410 # no setup, no teardown 

411 elif totalDuration is not None: 

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

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

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

415 totalDuration = testDuration 

416 # no test 

417 elif totalDuration is not None: 

418 testDuration = totalDuration 

419 if setupDuration is not None: 

420 testDuration -= setupDuration 

421 

422 if teardownDuration is not None: 

423 testDuration -= teardownDuration 

424 

425 if testDuration < timedelta(): 

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

427 

428 self._startTime = startTime 

429 self._setupDuration = setupDuration 

430 self._testDuration = testDuration 

431 self._teardownDuration = teardownDuration 

432 self._totalDuration = totalDuration 

433 

434 for parameterName, value in ( 

435 ("warningCount", warningCount), 

436 ("errorCount", errorCount), 

437 ("fatalCount", fatalCount), 

438 ("expectedWarningCount", expectedWarningCount), 

439 ("expectedErrorCount", expectedErrorCount), 

440 ("expectedFatalCount", expectedFatalCount) 

441 ): 

442 if not isinstance(value, int): 442 ↛ 443line 442 didn't jump to line 443 because the condition on line 442 was never true

443 ex = TypeError(f"Parameter '{parameterName}' is not of type 'int'.") 

444 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

445 raise ex 

446 

447 self._warningCount = warningCount 

448 self._errorCount = errorCount 

449 self._fatalCount = fatalCount 

450 self._expectedWarningCount = expectedWarningCount 

451 self._expectedErrorCount = expectedErrorCount 

452 self._expectedFatalCount = expectedFatalCount 

453 

454 for parameterName, value in (("title", title), ("summary", summary), ("description", description)): 

455 if value is not None and not isinstance(value, str): 

456 ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.") 

457 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

458 raise ex 

459 

460 self._title = title 

461 self._summary = summary 

462 self._description = description 

463 

464 if keyValuePairs is not None and not isinstance(keyValuePairs, Mapping): 

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

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

467 raise ex 

468 

469 self._dict = {} if keyValuePairs is None else dict(keyValuePairs) 

470 

471 # QUESTION: allow Parent as setter? 

472 @readonly 

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

474 """ 

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

476 

477 :returns: Reference to the parent entity. 

478 """ 

479 return self._parent 

480 

481 @readonly 

482 def Name(self) -> str: 

483 """ 

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

485 

486 :returns: The test entities name. 

487 """ 

488 return self._name 

489 

490 @readonly 

491 def Title(self) -> Nullable[str]: 

492 """ 

493 Read-only property to access the test entity's title (:attr:`_title`). 

494 

495 The title is a short label written for a reader, while the name identifies the test entity. 

496 

497 :returns: The test entity's title, or ``None`` if it wasn't recorded. 

498 """ 

499 return self._title 

500 

501 @readonly 

502 def Summary(self) -> Nullable[str]: 

503 """ 

504 Read-only property to access the test entity's summary (:attr:`_summary`). 

505 

506 :returns: The test entity's summary, or ``None`` if it wasn't recorded. 

507 """ 

508 return self._summary 

509 

510 @readonly 

511 def Description(self) -> Nullable[str]: 

512 """ 

513 Read-only property to access the test entity's description (:attr:`_description`). 

514 

515 :returns: The test entity's description, or ``None`` if it wasn't recorded. 

516 """ 

517 return self._description 

518 

519 @readonly 

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

521 """ 

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

523 

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

525 """ 

526 return self._startTime 

527 

528 @readonly 

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

530 """ 

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

532 

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

534 """ 

535 return self._setupDuration 

536 

537 @readonly 

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

539 """ 

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

541 

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

543 distinguishable, assign setup and teardown durations with zero. 

544 

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

546 """ 

547 return self._testDuration 

548 

549 @readonly 

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

551 """ 

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

553 

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

555 """ 

556 return self._teardownDuration 

557 

558 @readonly 

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

560 """ 

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

562 

563 this duration includes setup and teardown durations. 

564 

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

566 """ 

567 return self._totalDuration 

568 

569 @readonly 

570 def WarningCount(self) -> int: 

571 """ 

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

573 

574 :returns: Count of encountered warnings. 

575 """ 

576 return self._warningCount 

577 

578 @readonly 

579 def ErrorCount(self) -> int: 

580 """ 

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

582 

583 :returns: Count of encountered errors. 

584 """ 

585 return self._errorCount 

586 

587 @readonly 

588 def FatalCount(self) -> int: 

589 """ 

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

591 

592 :returns: Count of encountered fatal errors. 

593 """ 

594 return self._fatalCount 

595 

596 @readonly 

597 def ExpectedWarningCount(self) -> int: 

598 """ 

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

600 

601 :returns: Count of expected warnings. 

602 """ 

603 return self._expectedWarningCount 

604 

605 @readonly 

606 def ExpectedErrorCount(self) -> int: 

607 """ 

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

609 

610 :returns: Count of expected errors. 

611 """ 

612 return self._expectedErrorCount 

613 

614 @readonly 

615 def ExpectedFatalCount(self) -> int: 

616 """ 

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

618 

619 :returns: Count of expected fatal errors. 

620 """ 

621 return self._expectedFatalCount 

622 

623 def __len__(self) -> int: 

624 """ 

625 Returns the number of annotated key-value pairs. 

626 

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

628 """ 

629 return len(self._dict) 

630 

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

632 """ 

633 Access a key-value pair by key. 

634 

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

636 :returns: Value of the accessed key. 

637 """ 

638 return self._dict[key] 

639 

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

641 """ 

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

643 

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

645 

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

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

648 """ 

649 self._dict[key] = value 

650 

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

652 """ 

653 Delete a key-value pair by key. 

654 

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

656 """ 

657 del self._dict[key] 

658 

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

660 """ 

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

662 

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

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

665 """ 

666 return key in self._dict 

667 

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

669 """ 

670 Iterate all annotated key-value pairs. 

671 

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

673 """ 

674 yield from self._dict.items() 

675 

676 @abstractmethod 

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

678 """ 

679 Aggregate all test entities in the hierarchy. 

680 """ 

681 

682 @abstractmethod 

683 def __str__(self) -> str: 

684 """ 

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

686 """ 

687 

688 

689@export 

690class TestcaseOutputMixin(metaclass=ExtendedType, mixin=True): 

691 """ 

692 A mixin-class adding the output of a test case run: a message, details and the captured output streams. 

693 

694 A test case's status can be explained by a short message and details, like the message of a failed assertion and its 

695 traceback. These are the explanation of a failure, an error or why a test case was skipped. |br| 

696 In addition, the test case's standard output and standard error can be captured while it runs. 

697 

698 All fields are ``None``, if the information wasn't recorded. 

699 """ 

700 

701 _message: Nullable[str] #: Message explaining the test case's status (e.g. a failed assertion). 

702 _details: Nullable[str] #: Details explaining the test case's status (e.g. a traceback). 

703 _standardOutput: Nullable[str] #: Captured standard output of the test case. 

704 _standardError: Nullable[str] #: Captured standard error of the test case. 

705 

706 def __init__( 

707 self, 

708 message: Nullable[str] = None, 

709 details: Nullable[str] = None, 

710 standardOutput: Nullable[str] = None, 

711 standardError: Nullable[str] = None 

712 ) -> None: 

713 """ 

714 Initializes the fields of the mixin-class. 

715 

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

717 :param details: Optional, details explaining the test case's status. 

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

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

720 :raises TypeError: If parameter 'message' is not a string. 

721 :raises TypeError: If parameter 'details' is not a string. 

722 :raises TypeError: If parameter 'standardOutput' is not a string. 

723 :raises TypeError: If parameter 'standardError' is not a string. 

724 """ 

725 for parameterName, value in ( 

726 ("message", message), ("details", details), ("standardOutput", standardOutput), ("standardError", standardError) 

727 ): 

728 if value is not None and not isinstance(value, str): 

729 ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.") 

730 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

731 raise ex 

732 

733 self._message = message 

734 self._details = details 

735 self._standardOutput = standardOutput 

736 self._standardError = standardError 

737 

738 @readonly 

739 def Message(self) -> Nullable[str]: 

740 """ 

741 Read-only property to access the message explaining the test case's status (:attr:`_message`). 

742 

743 In a JUnit XML file, it's the ``message`` attribute of a ``<failure>``, ``<error>`` or ``<skipped>`` element. 

744 

745 :returns: The message, or ``None`` if it wasn't recorded. 

746 """ 

747 return self._message 

748 

749 @readonly 

750 def Details(self) -> Nullable[str]: 

751 """ 

752 Read-only property to access the details explaining the test case's status (:attr:`_details`). 

753 

754 In a JUnit XML file, it's the text of a ``<failure>``, ``<error>`` or ``<skipped>`` element, e.g. a traceback. 

755 

756 :returns: The details, or ``None`` if they weren't recorded. 

757 """ 

758 return self._details 

759 

760 @readonly 

761 def StandardOutput(self) -> Nullable[str]: 

762 """ 

763 Read-only property to access the captured standard output of the test case (:attr:`_standardOutput`). 

764 

765 In a JUnit XML file, it's the text of a ``<system-out>`` element. 

766 

767 :returns: The captured standard output, or ``None`` if it wasn't recorded. 

768 """ 

769 return self._standardOutput 

770 

771 @readonly 

772 def StandardError(self) -> Nullable[str]: 

773 """ 

774 Read-only property to access the captured standard error of the test case (:attr:`_standardError`). 

775 

776 In a JUnit XML file, it's the text of a ``<system-err>`` element. 

777 

778 :returns: The captured standard error, or ``None`` if it wasn't recorded. 

779 """ 

780 return self._standardError 

781 

782 

783@export 

784class Testcase(Base, TestcaseOutputMixin): 

785 """ 

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

787 

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

789 

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

791 

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

793 assertions (checks) as well as a sum thereof. The message and details explaining the status, as well as the 

794 captured output streams are provided by :class:`TestcaseOutputMixin`. 

795 """ 

796 

797 _status: TestcaseStatus 

798 _assertionCount: Nullable[int] 

799 _failedAssertionCount: Nullable[int] 

800 _passedAssertionCount: Nullable[int] 

801 

802 def __init__( 

803 self, 

804 name: str, 

805 title: Nullable[str] = None, 

806 summary: Nullable[str] = None, 

807 description: Nullable[str] = None, 

808 startTime: Nullable[datetime] = None, 

809 setupDuration: Nullable[timedelta] = None, 

810 testDuration: Nullable[timedelta] = None, 

811 teardownDuration: Nullable[timedelta] = None, 

812 totalDuration: Nullable[timedelta] = None, 

813 status: TestcaseStatus = TestcaseStatus.Unknown, 

814 assertionCount: Nullable[int] = None, 

815 failedAssertionCount: Nullable[int] = None, 

816 passedAssertionCount: Nullable[int] = None, 

817 warningCount: int = 0, 

818 errorCount: int = 0, 

819 fatalCount: int = 0, 

820 expectedWarningCount: int = 0, 

821 expectedErrorCount: int = 0, 

822 expectedFatalCount: int = 0, 

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

824 message: Nullable[str] = None, 

825 details: Nullable[str] = None, 

826 standardOutput: Nullable[str] = None, 

827 standardError: Nullable[str] = None, 

828 *, 

829 parent: Nullable[Testsuite] = None 

830 ) -> None: 

831 """ 

832 Initializes the fields of a test case. 

833 

834 :param name: Name of the test entity. 

835 :param title: Optional, short label of the test case, written for a reader. 

836 :param summary: Optional, summary of the test case. 

837 :param description: Optional, description of the test case. 

838 :param startTime: Optional, time when the test entity was started. 

839 :param setupDuration: Optional, duration it took to set up the entity. 

840 :param testDuration: Optional, duration of the entity's test run. 

841 :param teardownDuration: Optional, duration it took to tear down the entity. 

842 :param totalDuration: Optional, total duration of the entity's execution (setup + test + teardown) 

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

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

845 :param failedAssertionCount: Optional, number of failed assertions within the test. 

846 :param passedAssertionCount: Optional, number of passed assertions within the test. 

847 :param warningCount: Optional, count of encountered warnings. 

848 :param errorCount: Optional, count of encountered errors. 

849 :param fatalCount: Optional, count of encountered fatal errors. 

850 :param expectedWarningCount: Optional, count of expected warnings. 

851 :param expectedErrorCount: Optional, count of expected errors. 

852 :param expectedFatalCount: Optional, count of expected fatal errors. 

853 :param keyValuePairs: Optional, mapping of key-value pairs to initialize the test case. 

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

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

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

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

858 :param parent: Optional, reference to the parent test suite. 

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

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

861 """ 

862 

863 if parent is not None: 

864 if not isinstance(parent, Testsuite): 

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

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

867 raise ex 

868 

869 parent._testcases[name] = self 

870 

871 super().__init__( 

872 name, 

873 title, summary, description, 

874 startTime, 

875 setupDuration, testDuration, teardownDuration, totalDuration, 

876 warningCount, errorCount, fatalCount, 

877 expectedWarningCount, expectedErrorCount, expectedFatalCount, 

878 keyValuePairs, 

879 parent=parent 

880 ) 

881 TestcaseOutputMixin.__init__(self, message, details, standardOutput, standardError) 

882 

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

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

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

886 raise ex 

887 

888 self._status = status 

889 

890 for parameterName, value in ( 

891 ("assertionCount", assertionCount), 

892 ("failedAssertionCount", failedAssertionCount), 

893 ("passedAssertionCount", passedAssertionCount) 

894 ): 

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

896 ex = TypeError(f"Parameter '{parameterName}' is not of type 'int'.") 

897 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

898 raise ex 

899 

900 self._assertionCount = assertionCount 

901 if assertionCount is not None: 

902 if failedAssertionCount is not None: 

903 self._failedAssertionCount = failedAssertionCount 

904 

905 if passedAssertionCount is not None: 

906 if passedAssertionCount + failedAssertionCount != assertionCount: 

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

908 

909 self._passedAssertionCount = passedAssertionCount 

910 else: 

911 self._passedAssertionCount = assertionCount - failedAssertionCount 

912 elif passedAssertionCount is not None: 

913 self._passedAssertionCount = passedAssertionCount 

914 self._failedAssertionCount = assertionCount - passedAssertionCount 

915 else: 

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

917 elif failedAssertionCount is not None: 

918 self._failedAssertionCount = failedAssertionCount 

919 

920 if passedAssertionCount is not None: 

921 self._passedAssertionCount = passedAssertionCount 

922 self._assertionCount = passedAssertionCount + failedAssertionCount 

923 else: 

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

925 elif passedAssertionCount is not None: 

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

927 else: 

928 self._passedAssertionCount = None 

929 self._failedAssertionCount = None 

930 

931 @readonly 

932 def Status(self) -> TestcaseStatus: 

933 """ 

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

935 

936 :returns: The test case's status. 

937 """ 

938 return self._status 

939 

940 @readonly 

941 def AssertionCount(self) -> int: 

942 """ 

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

944 

945 :returns: Number of assertions. 

946 """ 

947 if self._assertionCount is None: 

948 return 0 

949 return self._assertionCount 

950 

951 @readonly 

952 def FailedAssertionCount(self) -> int: 

953 """ 

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

955 

956 :returns: Number of assertions. 

957 """ 

958 return self._failedAssertionCount 

959 

960 @readonly 

961 def PassedAssertionCount(self) -> int: 

962 """ 

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

964 

965 :returns: Number of passed assertions. 

966 """ 

967 return self._passedAssertionCount 

968 

969 def Copy(self) -> Testcase: 

970 return self.__class__( 

971 self._name, 

972 self._title, self._summary, self._description, 

973 self._startTime, 

974 self._setupDuration, 

975 self._testDuration, 

976 self._teardownDuration, 

977 self._totalDuration, 

978 self._status, 

979 self._assertionCount, 

980 self._failedAssertionCount, 

981 self._passedAssertionCount, 

982 warningCount=self._warningCount, 

983 errorCount=self._errorCount, 

984 fatalCount=self._fatalCount, 

985 expectedWarningCount=self._expectedWarningCount, 

986 expectedErrorCount=self._expectedErrorCount, 

987 expectedFatalCount=self._expectedFatalCount, 

988 message=self._message, 

989 details=self._details, 

990 standardOutput=self._standardOutput, 

991 standardError=self._standardError 

992 ) 

993 # TODO: copy key-value-pairs? 

994 

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

996 if self._status is TestcaseStatus.Unknown: 

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

998 self._status = TestcaseStatus.Passed 

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

1000 self._status = TestcaseStatus.Weak 

1001 elif self._failedAssertionCount == 0: 

1002 self._status = TestcaseStatus.Passed 

1003 else: 

1004 self._status = TestcaseStatus.Failed 

1005 

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

1007 self._status |= TestcaseStatus.Warned 

1008 

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

1010 self._status |= TestcaseStatus.Errored 

1011 

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

1013 self._status |= TestcaseStatus.Aborted 

1014 

1015 if strict: 

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

1017 

1018 # TODO: check for setup errors 

1019 # TODO: check for teardown errors 

1020 

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

1022 

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

1024 

1025 def __str__(self) -> str: 

1026 """ 

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

1028 

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

1030 

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

1032 """ 

1033 def formatDuration(duration: Nullable[timedelta]) -> str: 

1034 """ 

1035 Nested function formatting a duration in seconds. 

1036 

1037 :param duration: Duration to format. 

1038 :returns: Duration in seconds with three decimal places, or ``None``. 

1039 """ 

1040 return "None" if duration is None else f"{duration.total_seconds():.3f}" 

1041 

1042 return ( 

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

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

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

1046 f" setup/test/teardown:{formatDuration(self._setupDuration)}/{formatDuration(self._testDuration)}/" 

1047 f"{formatDuration(self._teardownDuration)}>" 

1048 ) 

1049 

1050 

1051@export 

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

1053 """ 

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

1055 

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

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

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

1059 """ 

1060 

1061 _kind: TestsuiteKind 

1062 _status: TestsuiteStatus 

1063 _testsuites: Dict[str, TestsuiteType] 

1064 

1065 _tests: int 

1066 _inconsistent: int 

1067 _excluded: int 

1068 _skipped: int 

1069 _errored: int 

1070 _weak: int 

1071 _failed: int 

1072 _passed: int 

1073 

1074 def __init__( 

1075 self, 

1076 name: str, 

1077 title: Nullable[str] = None, 

1078 summary: Nullable[str] = None, 

1079 description: Nullable[str] = None, 

1080 kind: TestsuiteKind = TestsuiteKind.Logical, 

1081 startTime: Nullable[datetime] = None, 

1082 setupDuration: Nullable[timedelta] = None, 

1083 testDuration: Nullable[timedelta] = None, 

1084 teardownDuration: Nullable[timedelta] = None, 

1085 totalDuration: Nullable[timedelta] = None, 

1086 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1087 warningCount: int = 0, 

1088 errorCount: int = 0, 

1089 fatalCount: int = 0, 

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

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

1092 *, 

1093 parent: Nullable[Testsuite] = None 

1094 ) -> None: 

1095 """ 

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

1097 

1098 :param name: Name of the test entity. 

1099 :param title: Optional, short label of the test entity, written for a reader. 

1100 :param summary: Optional, summary of the test entity. 

1101 :param description: Optional, description of the test entity. 

1102 :param kind: Optional, kind of the test entity. 

1103 :param startTime: Optional, time when the test entity was started. 

1104 :param setupDuration: Optional, duration it took to set up the entity. 

1105 :param testDuration: Optional, duration of all tests listed in the test entity. 

1106 :param teardownDuration: Optional, duration it took to tear down the entity. 

1107 :param totalDuration: Optional, total duration of the entity's execution (setup + test + teardown) 

1108 :param status: Optional, overall status of the test entity. 

1109 :param warningCount: Optional, count of encountered warnings incl. warnings from sub-elements. 

1110 :param errorCount: Optional, count of encountered errors incl. errors from sub-elements. 

1111 :param fatalCount: Optional, count of encountered fatal errors incl. fatal errors from sub-elements. 

1112 :param testsuites: Optional, list of test suites to initialize the test entity with. 

1113 :param keyValuePairs: Optional, mapping of key-value pairs to initialize the test entity with. 

1114 :param parent: Optional, reference to the parent test entity. 

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

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

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

1118 :raises AlreadyInTestsuiteError: If a test suite in parameter 'testsuites' is already part of a test entity 

1119 hierarchy. 

1120 :raises DuplicateTestsuiteError: If a test suite in parameter 'testsuites' is already listed (by name) in the list 

1121 of test suites. 

1122 """ 

1123 if parent is not None: 

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

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

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

1127 raise ex 

1128 

1129 parent._testsuites[name] = self 

1130 

1131 super().__init__( 

1132 name, 

1133 title, summary, description, 

1134 startTime, 

1135 setupDuration, 

1136 testDuration, 

1137 teardownDuration, 

1138 totalDuration, 

1139 warningCount, 

1140 errorCount, 

1141 fatalCount, 

1142 0, 0, 0, 

1143 keyValuePairs, 

1144 parent=parent 

1145 ) 

1146 

1147 self._kind = kind 

1148 self._status = status 

1149 

1150 self._testsuites = {} 

1151 if testsuites is not None: 

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

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

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

1155 raise ex 

1156 

1157 for testsuite in testsuites: 

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

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

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

1161 raise ex 

1162 

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

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

1165 

1166 if testsuite._name in self._testsuites: 

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

1168 

1169 testsuite._parent = self 

1170 self._testsuites[testsuite._name] = testsuite 

1171 

1172 self._tests = 0 

1173 self._inconsistent = 0 

1174 self._excluded = 0 

1175 self._skipped = 0 

1176 self._errored = 0 

1177 self._weak = 0 

1178 self._failed = 0 

1179 self._passed = 0 

1180 

1181 @readonly 

1182 def Kind(self) -> TestsuiteKind: 

1183 """ 

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

1185 

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

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

1188 language construct. 

1189 

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

1191 

1192 :returns: Kind of the test suite. 

1193 """ 

1194 return self._kind 

1195 

1196 @readonly 

1197 def Status(self) -> TestsuiteStatus: 

1198 """ 

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

1200 

1201 :returns: Overall status of the test suite. 

1202 """ 

1203 return self._status 

1204 

1205 @readonly 

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

1207 """ 

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

1209 

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

1211 """ 

1212 return self._testsuites 

1213 

1214 @readonly 

1215 def TestsuiteCount(self) -> int: 

1216 """ 

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

1218 

1219 :returns: Number of test suites. 

1220 """ 

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

1222 

1223 @readonly 

1224 def TestcaseCount(self) -> int: 

1225 """ 

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

1227 

1228 :returns: Number of test cases. 

1229 """ 

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

1231 

1232 @readonly 

1233 def AssertionCount(self) -> int: 

1234 """ 

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

1236 

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

1238 """ 

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

1240 

1241 @readonly 

1242 def FailedAssertionCount(self) -> int: 

1243 """ 

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

1245 

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

1247 """ 

1248 raise NotImplementedError() 

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

1250 

1251 @readonly 

1252 def PassedAssertionCount(self) -> int: 

1253 """ 

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

1255 

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

1257 """ 

1258 raise NotImplementedError() 

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

1260 

1261 @readonly 

1262 def Tests(self) -> int: 

1263 """ 

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

1265 

1266 :returns: Number of tests. 

1267 """ 

1268 return self._tests 

1269 

1270 @readonly 

1271 def Inconsistent(self) -> int: 

1272 """ 

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

1274 

1275 :returns: Number of inconsistent tests. 

1276 """ 

1277 return self._inconsistent 

1278 

1279 @readonly 

1280 def Excluded(self) -> int: 

1281 """ 

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

1283 

1284 :returns: Number of excluded tests. 

1285 """ 

1286 return self._excluded 

1287 

1288 @readonly 

1289 def Skipped(self) -> int: 

1290 """ 

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

1292 

1293 :returns: Number of skipped tests. 

1294 """ 

1295 return self._skipped 

1296 

1297 @readonly 

1298 def Errored(self) -> int: 

1299 """ 

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

1301 

1302 :returns: Number of errored tests. 

1303 """ 

1304 return self._errored 

1305 

1306 @readonly 

1307 def Weak(self) -> int: 

1308 """ 

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

1310 

1311 :returns: Number of weak tests. 

1312 """ 

1313 return self._weak 

1314 

1315 @readonly 

1316 def Failed(self) -> int: 

1317 """ 

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

1319 

1320 :returns: Number of failed tests. 

1321 """ 

1322 return self._failed 

1323 

1324 @readonly 

1325 def Passed(self) -> int: 

1326 """ 

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

1328 

1329 :returns: Number of passed tests. 

1330 """ 

1331 return self._passed 

1332 

1333 @readonly 

1334 def WarningCount(self) -> int: 

1335 """ 

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

1337 

1338 :returns: Number of warnings. 

1339 """ 

1340 raise NotImplementedError() 

1341 # return self._warningCount 

1342 

1343 @readonly 

1344 def ErrorCount(self) -> int: 

1345 """ 

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

1347 

1348 :returns: Number of errors. 

1349 """ 

1350 raise NotImplementedError() 

1351 # return self._errorCount 

1352 

1353 @readonly 

1354 def FatalCount(self) -> int: 

1355 """ 

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

1357 

1358 :returns: Number of fatal errors. 

1359 """ 

1360 raise NotImplementedError() 

1361 # return self._fatalCount 

1362 

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

1364 tests = 0 

1365 inconsistent = 0 

1366 excluded = 0 

1367 skipped = 0 

1368 errored = 0 

1369 weak = 0 

1370 failed = 0 

1371 passed = 0 

1372 

1373 warningCount = 0 

1374 errorCount = 0 

1375 fatalCount = 0 

1376 

1377 expectedWarningCount = 0 

1378 expectedErrorCount = 0 

1379 expectedFatalCount = 0 

1380 

1381 totalDuration = timedelta() 

1382 

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

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

1385 tests += t 

1386 inconsistent += i 

1387 excluded += ex 

1388 skipped += s 

1389 errored += e 

1390 weak += w 

1391 failed += f 

1392 passed += p 

1393 

1394 warningCount += wc 

1395 errorCount += ec 

1396 fatalCount += fc 

1397 

1398 expectedWarningCount += ewc 

1399 expectedErrorCount += eec 

1400 expectedFatalCount += efc 

1401 

1402 totalDuration += td 

1403 

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

1405 

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

1407 """ 

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

1409 

1410 :param testsuite: The test suite to add. 

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

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

1413 :raises AlreadyInTestsuiteError: If parameter 'testsuite' is already part of a test entity hierarchy. 

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

1415 """ 

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

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

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

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

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

1421 raise ex 

1422 

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

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

1425 

1426 if testsuite._name in self._testsuites: 

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

1428 

1429 testsuite._parent = self 

1430 self._testsuites[testsuite._name] = testsuite 

1431 

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

1433 """ 

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

1435 

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

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

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

1439 """ 

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

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

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

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

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

1445 raise ex 

1446 

1447 for testsuite in testsuites: 

1448 self.AddTestsuite(testsuite) 

1449 

1450 @abstractmethod 

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

1452 pass 

1453 

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

1455 return self.Iterate(scheme) 

1456 

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

1458 return self.Iterate(scheme) 

1459 

1460 def ToTree(self) -> Node: 

1461 rootNode = Node(value=self._name) 

1462 

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

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

1465 

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

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

1468 

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

1470 convertTestsuite(ts, testsuiteNode) 

1471 

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

1473 convertTestcase(tc, testsuiteNode) 

1474 

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

1476 convertTestsuite(testsuite, rootNode) 

1477 

1478 return rootNode 

1479 

1480 

1481@export 

1482class Testsuite(TestsuiteBase[TestsuiteType]): 

1483 """ 

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

1485 

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

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

1488 """ 

1489 

1490 _testcases: Dict[str, Testcase] 

1491 _hostname: Nullable[str] 

1492 

1493 def __init__( 

1494 self, 

1495 name: str, 

1496 title: Nullable[str] = None, 

1497 summary: Nullable[str] = None, 

1498 description: Nullable[str] = None, 

1499 kind: TestsuiteKind = TestsuiteKind.Logical, 

1500 hostname: Nullable[str] = None, 

1501 startTime: Nullable[datetime] = None, 

1502 setupDuration: Nullable[timedelta] = None, 

1503 testDuration: Nullable[timedelta] = None, 

1504 teardownDuration: Nullable[timedelta] = None, 

1505 totalDuration: Nullable[timedelta] = None, 

1506 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1507 warningCount: int = 0, 

1508 errorCount: int = 0, 

1509 fatalCount: int = 0, 

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

1511 testcases: Nullable[Iterable[Testcase]] = None, 

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

1513 *, 

1514 parent: Nullable[TestsuiteType] = None 

1515 ) -> None: 

1516 """ 

1517 Initializes the fields of a test suite. 

1518 

1519 :param name: Name of the test suite. 

1520 :param title: Optional, short label of the test suite, written for a reader. 

1521 :param summary: Optional, summary of the test suite. 

1522 :param description: Optional, description of the test suite. 

1523 :param kind: Optional, kind of the test suite. 

1524 :param hostname: Optional, name of the host the test suite was executed on, or ``None`` if it wasn't 

1525 recorded. 

1526 :param startTime: Optional, time when the test suite was started. 

1527 :param setupDuration: Optional, duration it took to set up the test suite. 

1528 :param testDuration: Optional, duration of all tests listed in the test suite. 

1529 :param teardownDuration: Optional, duration it took to tear down the test suite. 

1530 :param totalDuration: Optional, total duration of the entity's execution (setup + test + teardown) 

1531 :param status: Optional, overall status of the test suite. 

1532 :param warningCount: Optional, count of encountered warnings incl. warnings from sub-elements. 

1533 :param errorCount: Optional, count of encountered errors incl. errors from sub-elements. 

1534 :param fatalCount: Optional, count of encountered fatal errors incl. fatal errors from sub-elements. 

1535 :param testsuites: Optional, list of test suites to initialize the test suite with. 

1536 :param testcases: Optional, list of test cases to initialize the test suite with. 

1537 :param keyValuePairs: Optional, mapping of key-value pairs to initialize the test suite with. 

1538 :param parent: Optional, reference to the parent test entity. 

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

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

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

1542 :raises DuplicateTestcaseError: If a test case in parameter 'testcases' is already listed (by name) in the list of 

1543 test cases. 

1544 """ 

1545 super().__init__( 

1546 name, 

1547 title, summary, description, 

1548 kind, 

1549 startTime, 

1550 setupDuration, 

1551 testDuration, 

1552 teardownDuration, 

1553 totalDuration, 

1554 status, 

1555 warningCount, 

1556 errorCount, 

1557 fatalCount, 

1558 testsuites, 

1559 keyValuePairs, 

1560 parent=parent 

1561 ) 

1562 

1563 # self._testDuration = testDuration 

1564 self._hostname = hostname 

1565 

1566 self._testcases = {} 

1567 if testcases is not None: 

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

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

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

1571 raise ex 

1572 

1573 for testcase in testcases: 

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

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

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

1577 raise ex 

1578 

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

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

1581 

1582 if testcase._name in self._testcases: 

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

1584 

1585 testcase._parent = self 

1586 self._testcases[testcase._name] = testcase 

1587 

1588 @readonly 

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

1590 """ 

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

1592 

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

1594 """ 

1595 return self._testcases 

1596 

1597 @readonly 

1598 def TestcaseCount(self) -> int: 

1599 """ 

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

1601 

1602 :returns: Number of test cases. 

1603 """ 

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

1605 

1606 @readonly 

1607 def AssertionCount(self) -> int: 

1608 """ 

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

1610 

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

1612 """ 

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

1614 

1615 @readonly 

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

1617 """ 

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

1619 

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

1621 """ 

1622 return self._hostname 

1623 

1624 def Copy(self) -> Testsuite: 

1625 return self.__class__( 

1626 self._name, 

1627 self._title, self._summary, self._description, 

1628 self._kind, 

1629 self._hostname, 

1630 self._startTime, 

1631 self._setupDuration, 

1632 self._testDuration, 

1633 self._teardownDuration, 

1634 self._totalDuration, 

1635 self._status, 

1636 self._warningCount, 

1637 self._errorCount, 

1638 self._fatalCount 

1639 ) 

1640 

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

1642 ( 

1643 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, 

1644 warningCount, errorCount, fatalCount, 

1645 expectedWarningCount, expectedErrorCount, expectedFatalCount, 

1646 totalDuration 

1647 ) = super().Aggregate(strict) 

1648 

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

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

1651 

1652 tests += 1 

1653 

1654 warningCount += wc 

1655 errorCount += ec 

1656 fatalCount += fc 

1657 

1658 expectedWarningCount += ewc 

1659 expectedErrorCount += eec 

1660 expectedFatalCount += efc 

1661 

1662 totalDuration += td 

1663 

1664 status = testcase._status 

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

1666 raise UnittestError(f"Found testcase '{testcase._name}' with state 'Unknown'.") 

1667 elif TestcaseStatus.Inconsistent in status: 

1668 inconsistent += 1 

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

1670 excluded += 1 

1671 elif status is TestcaseStatus.Skipped: 

1672 skipped += 1 

1673 elif status is TestcaseStatus.Errored: 

1674 errored += 1 

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

1676 weak += 1 

1677 elif status is TestcaseStatus.Passed: 

1678 passed += 1 

1679 elif status is TestcaseStatus.Failed: 

1680 failed += 1 

1681 elif status is TestcaseStatus.ExpectedFailed: 

1682 passed += 1 

1683 elif status is TestcaseStatus.UnexpectedPassed: 1683 ↛ 1685line 1683 didn't jump to line 1685 because the condition on line 1683 was always true

1684 failed += 1 

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

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

1687 else: 

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

1689 

1690 self._tests = tests 

1691 self._inconsistent = inconsistent 

1692 self._excluded = excluded 

1693 self._skipped = skipped 

1694 self._errored = errored 

1695 self._weak = weak 

1696 self._failed = failed 

1697 self._passed = passed 

1698 

1699 self._warningCount = warningCount 

1700 self._errorCount = errorCount 

1701 self._fatalCount = fatalCount 

1702 

1703 self._expectedWarningCount = expectedWarningCount 

1704 self._expectedErrorCount = expectedErrorCount 

1705 self._expectedFatalCount = expectedFatalCount 

1706 

1707 if self._totalDuration is None: 

1708 self._totalDuration = totalDuration 

1709 

1710 if errored > 0: 

1711 self._status = TestsuiteStatus.Errored 

1712 elif failed > 0: 

1713 self._status = TestsuiteStatus.Failed 

1714 elif tests == 0: 

1715 self._status = TestsuiteStatus.Empty 

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

1717 self._status = TestsuiteStatus.Passed 

1718 elif tests == skipped: 

1719 self._status = TestsuiteStatus.Skipped 

1720 else: 

1721 self._status = TestsuiteStatus.Unknown 

1722 

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

1724 

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

1726 """ 

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

1728 

1729 :param testcase: The test case to add. 

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

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

1732 :raises AlreadyInTestsuiteError: If parameter 'testcase' is already part of a test entity hierarchy. 

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

1734 """ 

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

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

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

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

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

1740 raise ex 

1741 

1742 if testcase._parent is not None: 

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

1744 

1745 if testcase._name in self._testcases: 

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

1747 

1748 testcase._parent = self 

1749 self._testcases[testcase._name] = testcase 

1750 

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

1752 """ 

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

1754 

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

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

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

1758 """ 

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

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

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

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

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

1764 raise ex 

1765 

1766 for testcase in testcases: 

1767 self.AddTestcase(testcase) 

1768 

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

1770 if IterationScheme.PreOrder in scheme: 

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

1772 yield self 

1773 

1774 if IterationScheme.IncludeTestcases in scheme: 

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

1776 yield testcase 

1777 

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

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

1780 

1781 if IterationScheme.PostOrder in scheme: 

1782 if IterationScheme.IncludeTestcases in scheme: 

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

1784 yield testcase 

1785 

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

1787 yield self 

1788 

1789 def __str__(self) -> str: 

1790 return ( 

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

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

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

1794 ) 

1795 

1796 

1797@export 

1798class TestsuiteSummary(TestsuiteBase[TestsuiteType]): 

1799 """ 

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

1801 

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

1803 """ 

1804 

1805 def __init__( 

1806 self, 

1807 name: str, 

1808 title: Nullable[str] = None, 

1809 summary: Nullable[str] = None, 

1810 description: Nullable[str] = None, 

1811 startTime: Nullable[datetime] = None, 

1812 setupDuration: Nullable[timedelta] = None, 

1813 testDuration: Nullable[timedelta] = None, 

1814 teardownDuration: Nullable[timedelta] = None, 

1815 totalDuration: Nullable[timedelta] = None, 

1816 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1817 warningCount: int = 0, 

1818 errorCount: int = 0, 

1819 fatalCount: int = 0, 

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

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

1822 *, 

1823 parent: Nullable[TestsuiteType] = None 

1824 ) -> None: 

1825 """ 

1826 Initializes the fields of a test summary. 

1827 

1828 :param name: Name of the test summary. 

1829 :param title: Optional, short label of the test summary, written for a reader. 

1830 :param summary: Optional, summary of the test summary. 

1831 :param description: Optional, description of the test summary. 

1832 :param startTime: Optional, time when the test summary was started. 

1833 :param setupDuration: Optional, duration it took to set up the test summary. 

1834 :param testDuration: Optional, duration of all tests listed in the test summary. 

1835 :param teardownDuration: Optional, duration it took to tear down the test summary. 

1836 :param totalDuration: Optional, total duration of the entity's execution (setup + test + teardown) 

1837 :param status: Optional, overall status of the test summary. 

1838 :param warningCount: Optional, count of encountered warnings incl. warnings from sub-elements. 

1839 :param errorCount: Optional, count of encountered errors incl. errors from sub-elements. 

1840 :param fatalCount: Optional, count of encountered fatal errors incl. fatal errors from sub-elements. 

1841 :param testsuites: Optional, list of test suites to initialize the test summary with. 

1842 :param keyValuePairs: Optional, mapping of key-value pairs to initialize the test summary with. 

1843 :param parent: Optional, reference to the parent test summary. 

1844 """ 

1845 super().__init__( 

1846 name, 

1847 title, summary, description, 

1848 TestsuiteKind.Root, 

1849 startTime, setupDuration, testDuration, teardownDuration, totalDuration, 

1850 status, 

1851 warningCount, errorCount, fatalCount, 

1852 testsuites, 

1853 keyValuePairs, 

1854 parent=parent 

1855 ) 

1856 

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

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

1859 

1860 self._tests = tests 

1861 self._inconsistent = inconsistent 

1862 self._excluded = excluded 

1863 self._skipped = skipped 

1864 self._errored = errored 

1865 self._weak = weak 

1866 self._failed = failed 

1867 self._passed = passed 

1868 

1869 self._warningCount = warningCount 

1870 self._errorCount = errorCount 

1871 self._fatalCount = fatalCount 

1872 

1873 self._expectedWarningCount = expectedWarningCount 

1874 self._expectedErrorCount = expectedErrorCount 

1875 self._expectedFatalCount = expectedFatalCount 

1876 

1877 if self._totalDuration is None: 

1878 self._totalDuration = totalDuration 

1879 

1880 if errored > 0: 

1881 self._status = TestsuiteStatus.Errored 

1882 elif failed > 0: 

1883 self._status = TestsuiteStatus.Failed 

1884 elif tests == 0: 

1885 self._status = TestsuiteStatus.Empty 

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

1887 self._status = TestsuiteStatus.Passed 

1888 elif tests == skipped: 

1889 self._status = TestsuiteStatus.Skipped 

1890 elif tests == excluded: 

1891 self._status = TestsuiteStatus.Excluded 

1892 else: 

1893 self._status = TestsuiteStatus.Unknown 

1894 

1895 return ( 

1896 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, 

1897 warningCount, errorCount, fatalCount, 

1898 expectedWarningCount, expectedErrorCount, expectedFatalCount, 

1899 totalDuration 

1900 ) 

1901 

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

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

1904 yield self 

1905 

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

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

1908 

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

1910 yield self 

1911 

1912 def __str__(self) -> str: 

1913 return ( 

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

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

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

1917 ) 

1918 

1919 

1920@export 

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

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

1923 

1924 _path: Path #: Path to the report file. 

1925 

1926 _analysisDuration: Nullable[timedelta] #: Duration of :meth:`Analyze`, or ``None`` before it ran. 

1927 _modelConversion: Nullable[timedelta] #: Duration of :meth:`Convert`, or ``None`` before it ran. 

1928 

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

1930 self._path = reportFile 

1931 

1932 self._analysisDuration = None 

1933 self._modelConversion = None 

1934 

1935 if analyzeAndConvert: 

1936 self.Analyze() 

1937 self.Convert() 

1938 

1939 @readonly 

1940 def Path(self) -> Path: 

1941 """ 

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

1943 

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

1945 """ 

1946 return self._path 

1947 

1948 @readonly 

1949 def AnalysisDuration(self) -> Nullable[timedelta]: 

1950 """ 

1951 Read-only property to access the analysis duration (:attr:`_analysisDuration`). 

1952 

1953 .. note:: 

1954 

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

1956 content to the test entity hierarchy. 

1957 

1958 :returns: Duration to analyze the document, or ``None`` if it wasn't analyzed yet. 

1959 """ 

1960 return self._analysisDuration 

1961 

1962 @readonly 

1963 def ModelConversionDuration(self) -> Nullable[timedelta]: 

1964 """ 

1965 Read-only property to access the conversion duration (:attr:`_modelConversion`). 

1966 

1967 .. note:: 

1968 

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

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

1971 

1972 :returns: Duration to convert the document, or ``None`` if it wasn't converted yet. 

1973 """ 

1974 return self._modelConversion 

1975 

1976 @abstractmethod 

1977 def Analyze(self) -> None: 

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

1979 

1980 # @abstractmethod 

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

1982 # pass 

1983 

1984 @abstractmethod 

1985 def Convert(self): 

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

1987 

1988 

1989@export 

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

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

1992 

1993 _mergedCount: int 

1994 

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

1996 self._mergedCount = mergedCount 

1997 

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

1999 """ 

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

2001 

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

2003 nothing rather than erasing what is known. 

2004 

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

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

2007 """ 

2008 if otherStartTime is None: 

2009 return self._startTime 

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

2011 return otherStartTime 

2012 else: 

2013 return self._startTime 

2014 

2015 def _MergeTexts(self, other: Base) -> None: 

2016 """ 

2017 Take over the title, summary and description of an entity being merged in, where this entity has none. 

2018 

2019 :param other: The entity being merged in. 

2020 """ 

2021 if self._title is None: 2021 ↛ 2024line 2021 didn't jump to line 2024 because the condition on line 2021 was always true

2022 self._title = other._title 

2023 

2024 if self._summary is None: 2024 ↛ 2027line 2024 didn't jump to line 2027 because the condition on line 2024 was always true

2025 self._summary = other._summary 

2026 

2027 if self._description is None: 2027 ↛ exitline 2027 didn't return from function '_MergeTexts' because the condition on line 2027 was always true

2028 self._description = other._description 

2029 

2030 @readonly 

2031 def MergedCount(self) -> int: 

2032 """ 

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

2034 

2035 :returns: Number of merged entities. 

2036 """ 

2037 return self._mergedCount 

2038 

2039 

2040@export 

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

2042 _combinedCount: int 

2043 

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

2045 self._combinedCount = combinedCound 

2046 

2047 @readonly 

2048 def CombinedCount(self) -> int: 

2049 """ 

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

2051 

2052 :returns: Number of combined entities. 

2053 """ 

2054 return self._combinedCount 

2055 

2056 

2057@export 

2058class MergedTestcase(Testcase, Merged): 

2059 """ 

2060 A test case merged from the same test case found in multiple test reports. 

2061 

2062 The message, details and captured output streams (see :class:`TestcaseOutputMixin`) as well as the title, summary 

2063 and description are taken from the first merged test case that has them. 

2064 """ 

2065 

2066 _mergedTestcases: List[Testcase] #: List of test cases merged into this test case. 

2067 

2068 def __init__( 

2069 self, 

2070 testcase: Testcase, 

2071 *, 

2072 parent: Nullable[Testsuite] = None 

2073 ) -> None: 

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

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

2076 

2077 super().__init__( 

2078 testcase._name, 

2079 testcase._title, testcase._summary, testcase._description, 

2080 testcase._startTime, 

2081 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration, 

2082 TestcaseStatus.Unknown, 

2083 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount, 

2084 testcase._warningCount, testcase._errorCount, testcase._fatalCount, 

2085 testcase._expectedWarningCount, testcase._expectedErrorCount, testcase._expectedFatalCount, 

2086 message=testcase._message, 

2087 details=testcase._details, 

2088 standardOutput=testcase._standardOutput, 

2089 standardError=testcase._standardError, 

2090 parent=parent 

2091 ) 

2092 Merged.__init__(self) 

2093 

2094 self._mergedTestcases = [testcase] 

2095 

2096 @readonly 

2097 def Status(self) -> TestcaseStatus: 

2098 """ 

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

2100 

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

2102 """ 

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

2104 status = self._mergedTestcases[0]._status 

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

2106 status @= mtc._status 

2107 

2108 self._status = status 

2109 

2110 return self._status 

2111 

2112 @readonly 

2113 def SummedAssertionCount(self) -> int: 

2114 """ 

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

2116 

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

2118 """ 

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

2120 

2121 @readonly 

2122 def SummedPassedAssertionCount(self) -> int: 

2123 """ 

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

2125 

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

2127 """ 

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

2129 

2130 @readonly 

2131 def SummedFailedAssertionCount(self) -> int: 

2132 """ 

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

2134 

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

2136 """ 

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

2138 

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

2140 firstMTC = self._mergedTestcases[0] 

2141 

2142 status = firstMTC._status 

2143 warningCount = firstMTC._warningCount 

2144 errorCount = firstMTC._errorCount 

2145 fatalCount = firstMTC._fatalCount 

2146 totalDuration = firstMTC._totalDuration 

2147 

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

2149 status @= mtc._status 

2150 warningCount += mtc._warningCount 

2151 errorCount += mtc._errorCount 

2152 fatalCount += mtc._fatalCount 

2153 

2154 self._status = status 

2155 

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

2157 

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

2159 """ 

2160 Merge another occurrence of this test case into this merged test case. 

2161 

2162 Warning, error and fatal counts are summed up. The message, details, captured output streams, title, summary and 

2163 description are kept from the first merged test case that has them. 

2164 

2165 :param tc: The test case to merge. 

2166 """ 

2167 self._mergedCount += 1 

2168 

2169 self._mergedTestcases.append(tc) 

2170 

2171 self._warningCount += tc._warningCount 

2172 self._errorCount += tc._errorCount 

2173 self._fatalCount += tc._fatalCount 

2174 

2175 if self._message is None: 

2176 self._message = tc._message 

2177 

2178 if self._details is None: 

2179 self._details = tc._details 

2180 

2181 if self._standardOutput is None: 

2182 self._standardOutput = tc._standardOutput 

2183 

2184 if self._standardError is None: 2184 ↛ 2187line 2184 didn't jump to line 2187 because the condition on line 2184 was always true

2185 self._standardError = tc._standardError 

2186 

2187 self._MergeTexts(tc) 

2188 

2189 def ToTestcase(self) -> Testcase: 

2190 return Testcase( 

2191 self._name, 

2192 self._title, self._summary, self._description, 

2193 self._startTime, 

2194 self._setupDuration, 

2195 self._testDuration, 

2196 self._teardownDuration, 

2197 self._totalDuration, 

2198 self._status, 

2199 self._assertionCount, 

2200 self._failedAssertionCount, 

2201 self._passedAssertionCount, 

2202 self._warningCount, 

2203 self._errorCount, 

2204 self._fatalCount, 

2205 message=self._message, 

2206 details=self._details, 

2207 standardOutput=self._standardOutput, 

2208 standardError=self._standardError 

2209 ) 

2210 

2211 

2212@export 

2213class MergedTestsuite(Testsuite, Merged): 

2214 def __init__( 

2215 self, 

2216 testsuite: Testsuite, 

2217 addTestsuites: bool = False, 

2218 addTestcases: bool = False, 

2219 *, 

2220 parent: Nullable[Testsuite] = None 

2221 ) -> None: 

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

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

2224 

2225 super().__init__( 

2226 testsuite._name, 

2227 testsuite._title, testsuite._summary, testsuite._description, 

2228 testsuite._kind, 

2229 testsuite._hostname, 

2230 testsuite._startTime, 

2231 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration, 

2232 TestsuiteStatus.Unknown, 

2233 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount, 

2234 parent=parent 

2235 ) 

2236 Merged.__init__(self) 

2237 

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

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

2240 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases) 

2241 self.AddTestsuite(mergedTestsuite) 

2242 

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

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

2245 mergedTestcase = MergedTestcase(tc) 

2246 self.AddTestcase(mergedTestcase) 

2247 

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

2249 """ 

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

2251 

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

2253 unanimous hostname. 

2254 

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

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

2257 """ 

2258 if otherHostname is None: 

2259 return self._hostname 

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

2261 return otherHostname 

2262 else: 

2263 return "various" 

2264 

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

2266 self._mergedCount += 1 

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

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

2269 self._MergeTexts(testsuite) 

2270 

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

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

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

2274 else: 

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

2276 self.AddTestsuite(mergedTestsuite) 

2277 

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

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

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

2281 else: 

2282 mergedTestcase = MergedTestcase(tc) 

2283 self.AddTestcase(mergedTestcase) 

2284 

2285 def ToTestsuite(self) -> Testsuite: 

2286 testsuite = Testsuite( 

2287 self._name, 

2288 self._title, self._summary, self._description, 

2289 self._kind, 

2290 self._hostname, 

2291 self._startTime, 

2292 self._setupDuration, 

2293 self._testDuration, 

2294 self._teardownDuration, 

2295 self._totalDuration, 

2296 self._status, 

2297 self._warningCount, 

2298 self._errorCount, 

2299 self._fatalCount, 

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

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

2302 ) 

2303 

2304 testsuite._tests = self._tests 

2305 testsuite._excluded = self._excluded 

2306 testsuite._inconsistent = self._inconsistent 

2307 testsuite._skipped = self._skipped 

2308 testsuite._errored = self._errored 

2309 testsuite._weak = self._weak 

2310 testsuite._failed = self._failed 

2311 testsuite._passed = self._passed 

2312 

2313 return testsuite 

2314 

2315 

2316@export 

2317class MergedTestsuiteSummary(TestsuiteSummary, Merged): 

2318 _mergedFiles: Dict[Path, TestsuiteSummary] 

2319 

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

2321 super().__init__(name) 

2322 Merged.__init__(self, mergedCount=0) 

2323 

2324 self._mergedFiles = {} 

2325 

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

2327 # if summary.File in self._mergedFiles: 

2328 # raise 

2329 

2330 # FIXME: a summary is not necessarily a file 

2331 self._mergedCount += 1 

2332 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary 

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

2334 self._MergeTexts(testsuiteSummary) 

2335 

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

2337 if testsuite._name in self._testsuites: 

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

2339 else: 

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

2341 self.AddTestsuite(mergedTestsuite) 

2342 

2343 def ToTestsuiteSummary(self) -> TestsuiteSummary: 

2344 testsuiteSummary = TestsuiteSummary( 

2345 self._name, 

2346 self._title, self._summary, self._description, 

2347 self._startTime, 

2348 self._setupDuration, 

2349 self._testDuration, 

2350 self._teardownDuration, 

2351 self._totalDuration, 

2352 self._status, 

2353 self._warningCount, 

2354 self._errorCount, 

2355 self._fatalCount, 

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

2357 ) 

2358 

2359 testsuiteSummary._tests = self._tests 

2360 testsuiteSummary._excluded = self._excluded 

2361 testsuiteSummary._inconsistent = self._inconsistent 

2362 testsuiteSummary._skipped = self._skipped 

2363 testsuiteSummary._errored = self._errored 

2364 testsuiteSummary._weak = self._weak 

2365 testsuiteSummary._failed = self._failed 

2366 testsuiteSummary._passed = self._passed 

2367 

2368 return testsuiteSummary