Coverage for pyEDAA/Reports/Unittesting/__init__.py: 79%
797 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 22:20 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 22:20 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2024-2025 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32The pyEDAA.Reports.Unittesting package implements a hierarchy of test entities. These are test cases, test suites and a
33test summary provided as a class hierarchy. Test cases are the leaf elements in the hierarchy and abstract an
34individual test run. Test suites are used to group multiple test cases or other test suites. The root element is a test
35summary. When such a summary is stored in a file format like Ant + JUnit4 XML, a file format specific document is
36derived from a summary class.
38**Data Model**
40.. mermaid::
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]
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
70 classDef root fill:#4dc3ff
71 classDef summary fill:#80d4ff
72 classDef suite fill:#b3e6ff
73 classDef case fill:#eeccff
74"""
75from datetime import timedelta, datetime
76from enum import Flag, IntEnum
77from pathlib import Path
78from sys import version_info
79from typing import Optional as Nullable, Dict, Iterable, Any, Tuple, Generator, Union, List, Generic, TypeVar, Mapping
81from pyTooling.Common import getFullyQualifiedName
82from pyTooling.Decorators import export, readonly
83from pyTooling.MetaClasses import ExtendedType, abstractmethod
84from pyTooling.Tree import Node
86from pyEDAA.Reports import ReportException
89@export
90class UnittestException(ReportException):
91 """Base-exception for all unit test related exceptions."""
94@export
95class AlreadyInHierarchyException(UnittestException):
96 """
97 A unit test exception raised if the element is already part of a hierarchy.
99 This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same
100 hierarchy should occur only once in the hierarchy.
102 .. hint::
104 This is usually caused by a non-None parent reference.
105 """
108@export
109class DuplicateTestsuiteException(UnittestException):
110 """
111 A unit test exception raised on duplicate test suites (by name).
113 This exception is raised, if a child test suite with same name already exist in the test suite.
115 .. hint::
117 Test suite names need to be unique per parent element (test suite or test summary).
118 """
121@export
122class DuplicateTestcaseException(UnittestException):
123 """
124 A unit test exception raised on duplicate test cases (by name).
126 This exception is raised, if a child test case with same name already exist in the test suite.
128 .. hint::
130 Test case names need to be unique per parent element (test suite).
131 """
134@export
135class TestcaseStatus(Flag):
136 """A flag enumeration describing the status of a test case."""
137 Unknown = 0 #: Testcase status is uninitialized and therefore unknown.
138 Excluded = 1 #: Testcase was permanently excluded / disabled
139 Skipped = 2 #: Testcase was temporarily skipped (e.g. based on a condition)
140 Weak = 4 #: No assertions were recorded.
141 Passed = 8 #: A passed testcase, because all assertions were successful.
142 Failed = 16 #: A failed testcase due to at least one failed assertion.
144 Mask = Excluded | Skipped | Weak | Passed | Failed
146 Inverted = 128 #: To mark inverted results
147 UnexpectedPassed = Failed | Inverted
148 ExpectedFailed = Passed | Inverted
150 Warned = 1024 #: Runtime warning
151 Errored = 2048 #: Runtime error (mostly caught exceptions)
152 Aborted = 4096 #: Uncaught runtime exception
154 SetupError = 8192 #: Preparation / compilation error
155 TearDownError = 16384 #: Cleanup error / resource release error
156 Inconsistent = 32768 #: Dataset is inconsistent
158 Flags = Warned | Errored | Aborted | SetupError | TearDownError | Inconsistent
160 # TODO: timed out ?
161 # TODO: some passed (if merged, mixed results of passed and failed)
163 def __matmul__(self, other: "TestcaseStatus") -> "TestcaseStatus":
164 s = self & self.Mask
165 o = other & self.Mask
166 if s is self.Excluded: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 resolved = self.Excluded if o is self.Excluded else self.Unknown
168 elif s is self.Skipped:
169 resolved = self.Unknown if (o is self.Unknown) or (o is self.Excluded) else o
170 elif s is self.Weak: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 resolved = self.Weak if o is self.Weak else self.Unknown
172 elif s is self.Passed: 172 ↛ 177line 172 didn't jump to line 177 because the condition on line 172 was always true
173 if o is self.Failed: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true
174 resolved = self.Failed
175 else:
176 resolved = self.Passed if (o is self.Skipped) or (o is self.Passed) else self.Unknown
177 elif s is self.Failed:
178 resolved = self.Failed if (o is self.Skipped) or (o is self.Passed) or (o is self.Failed) else self.Unknown
179 else:
180 resolved = self.Unknown
182 resolved |= (self & self.Flags) | (other & self.Flags)
183 return resolved
186@export
187class TestsuiteStatus(Flag):
188 """A flag enumeration describing the status of a test suite."""
189 Unknown = 0
190 Excluded = 1 #: Testcase was permanently excluded / disabled
191 Skipped = 2 #: Testcase was temporarily skipped (e.g. based on a condition)
192 Empty = 4 #: No tests in suite
193 Passed = 8 #: Passed testcase, because all assertions succeeded
194 Failed = 16 #: Failed testcase due to failing assertions
196 Mask = Excluded | Skipped | Empty | Passed | Failed
198 Inverted = 128 #: To mark inverted results
199 UnexpectedPassed = Failed | Inverted
200 ExpectedFailed = Passed | Inverted
202 Warned = 1024 #: Runtime warning
203 Errored = 2048 #: Runtime error (mostly caught exceptions)
204 Aborted = 4096 #: Uncaught runtime exception
206 SetupError = 8192 #: Preparation / compilation error
207 TearDownError = 16384 #: Cleanup error / resource release error
209 Flags = Warned | Errored | Aborted | SetupError | TearDownError
212@export
213class TestsuiteKind(IntEnum):
214 """Enumeration describing the kind of test suite."""
215 Root = 0 #: Root element of the hierarchy.
216 Logical = 1 #: Represents a logical unit.
217 Namespace = 2 #: Represents a namespace.
218 Package = 3 #: Represents a package.
219 Module = 4 #: Represents a module.
220 Class = 5 #: Represents a class.
223@export
224class IterationScheme(Flag):
225 """
226 A flag enumeration for selecting the test suite iteration scheme.
228 When a test entity hierarchy is (recursively) iterated, this iteration scheme describes how to iterate the hierarchy
229 and what elements to return as a result.
230 """
231 Unknown = 0 #: Neutral element.
232 IncludeSelf = 1 #: Also include the element itself.
233 IncludeTestsuites = 2 #: Include test suites into the result.
234 IncludeTestcases = 4 #: Include test cases into the result.
236 Recursive = 8 #: Iterate recursively.
238 PreOrder = 16 #: Iterate in pre-order (top-down: current node, then child element left-to-right).
239 PostOrder = 32 #: Iterate in pre-order (bottom-up: child element left-to-right, then current node).
241 Default = IncludeTestsuites | Recursive | IncludeTestcases | PreOrder #: Recursively iterate all test entities in pre-order.
242 TestsuiteDefault = IncludeTestsuites | Recursive | PreOrder #: Recursively iterate only test suites in pre-order.
243 TestcaseDefault = IncludeTestcases | Recursive | PreOrder #: Recursively iterate only test cases in pre-order.
246TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite")
247TestcaseAggregateReturnType = Tuple[int, int, int, timedelta]
248TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int, int, int, int, int, int, timedelta]
251@export
252class Base(metaclass=ExtendedType, slots=True):
253 """
254 Base-class for all test entities (test cases, test suites, ...).
256 It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity
257 hierarchy.
259 Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the
260 child. |br|
261 E.g. it's used as a test case name in the dictionary of test cases in a test suite.
263 Every test entity has fields for time tracking. If known, a start time and a test duration can be set. For more
264 details, a setup duration and teardown duration can be added. All durations are summed up in a total duration field.
266 As tests can have warnings and errors or even fail, these messages are counted and aggregated in the test entity
267 hierarchy.
269 Every test entity offers an internal dictionary for annotations. |br|
270 This feature is for example used by Ant + JUnit4's XML property fields.
271 """
273 _parent: Nullable["TestsuiteBase"]
274 _name: str
276 _startTime: Nullable[datetime]
277 _setupDuration: Nullable[timedelta]
278 _testDuration: Nullable[timedelta]
279 _teardownDuration: Nullable[timedelta]
280 _totalDuration: Nullable[timedelta]
282 _warningCount: int
283 _errorCount: int
284 _fatalCount: int
286 _dict: Dict[str, Any]
288 def __init__(
289 self,
290 name: str,
291 startTime: Nullable[datetime] = None,
292 setupDuration: Nullable[timedelta] = None,
293 testDuration: Nullable[timedelta] = None,
294 teardownDuration: Nullable[timedelta] = None,
295 totalDuration: Nullable[timedelta] = None,
296 warningCount: int = 0,
297 errorCount: int = 0,
298 fatalCount: int = 0,
299 keyValuePairs: Nullable[Mapping[str, Any]] = None,
300 parent: Nullable["TestsuiteBase"] = None
301 ):
302 """
303 Initializes the fields of the base-class.
305 :param name: Name of the test entity.
306 :param startTime: Time when the test entity was started.
307 :param setupDuration: Duration it took to set up the entity.
308 :param testDuration: Duration of the entity's test run.
309 :param teardownDuration: Duration it took to tear down the entity.
310 :param totalDuration: Total duration of the entity's execution (setup + test + teardown).
311 :param warningCount: Count of encountered warnings.
312 :param errorCount: Count of encountered errors.
313 :param fatalCount: Count of encountered fatal errors.
314 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with.
315 :param parent: Reference to the parent test entity.
316 :raises TypeError: If parameter 'parent' is not a TestsuiteBase.
317 :raises ValueError: If parameter 'name' is None.
318 :raises TypeError: If parameter 'name' is not a string.
319 :raises ValueError: If parameter 'name' is empty.
320 :raises TypeError: If parameter 'testDuration' is not a timedelta.
321 :raises TypeError: If parameter 'setupDuration' is not a timedelta.
322 :raises TypeError: If parameter 'teardownDuration' is not a timedelta.
323 :raises TypeError: If parameter 'totalDuration' is not a timedelta.
324 :raises TypeError: If parameter 'warningCount' is not an integer.
325 :raises TypeError: If parameter 'errorCount' is not an integer.
326 :raises TypeError: If parameter 'fatalCount' is not an integer.
327 :raises TypeError: If parameter 'keyValuePairs' is not a Mapping.
328 :raises ValueError: If parameter 'totalDuration' is not consistent.
329 """
331 if parent is not None and not isinstance(parent, TestsuiteBase): 331 ↛ 332line 331 didn't jump to line 332 because the condition on line 331 was never true
332 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
333 if version_info >= (3, 11): # pragma: no cover
334 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
335 raise ex
337 if name is None:
338 raise ValueError(f"Parameter 'name' is None.")
339 elif not isinstance(name, str): 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true
340 ex = TypeError(f"Parameter 'name' is not of type 'str'.")
341 if version_info >= (3, 11): # pragma: no cover
342 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
343 raise ex
344 elif name.strip() == "": 344 ↛ 345line 344 didn't jump to line 345 because the condition on line 344 was never true
345 raise ValueError(f"Parameter 'name' is empty.")
347 self._parent = parent
348 self._name = name
350 if testDuration is not None and not isinstance(testDuration, timedelta): 350 ↛ 351line 350 didn't jump to line 351 because the condition on line 350 was never true
351 ex = TypeError(f"Parameter 'testDuration' is not of type 'timedelta'.")
352 if version_info >= (3, 11): # pragma: no cover
353 ex.add_note(f"Got type '{getFullyQualifiedName(testDuration)}'.")
354 raise ex
356 if setupDuration is not None and not isinstance(setupDuration, timedelta): 356 ↛ 357line 356 didn't jump to line 357 because the condition on line 356 was never true
357 ex = TypeError(f"Parameter 'setupDuration' is not of type 'timedelta'.")
358 if version_info >= (3, 11): # pragma: no cover
359 ex.add_note(f"Got type '{getFullyQualifiedName(setupDuration)}'.")
360 raise ex
362 if teardownDuration is not None and not isinstance(teardownDuration, timedelta): 362 ↛ 363line 362 didn't jump to line 363 because the condition on line 362 was never true
363 ex = TypeError(f"Parameter 'teardownDuration' is not of type 'timedelta'.")
364 if version_info >= (3, 11): # pragma: no cover
365 ex.add_note(f"Got type '{getFullyQualifiedName(teardownDuration)}'.")
366 raise ex
368 if totalDuration is not None and not isinstance(totalDuration, timedelta): 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true
369 ex = TypeError(f"Parameter 'totalDuration' is not of type 'timedelta'.")
370 if version_info >= (3, 11): # pragma: no cover
371 ex.add_note(f"Got type '{getFullyQualifiedName(totalDuration)}'.")
372 raise ex
374 if testDuration is not None:
375 if setupDuration is not None: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true
376 if teardownDuration is not None:
377 if totalDuration is not None:
378 if totalDuration < (setupDuration + testDuration + teardownDuration):
379 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup, test and teardown durations.")
380 else: # no total
381 totalDuration = setupDuration + testDuration + teardownDuration
382 # no teardown
383 elif totalDuration is not None:
384 if totalDuration < (setupDuration + testDuration):
385 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup and test durations.")
386 # no teardown, no total
387 else:
388 totalDuration = setupDuration + testDuration
389 # no setup
390 elif teardownDuration is not None: 390 ↛ 391line 390 didn't jump to line 391 because the condition on line 390 was never true
391 if totalDuration is not None:
392 if totalDuration < (testDuration + teardownDuration):
393 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of test and teardown durations.")
394 else: # no setup, no total
395 totalDuration = testDuration + teardownDuration
396 # no setup, no teardown
397 elif totalDuration is not None:
398 if totalDuration < testDuration: 398 ↛ 399line 398 didn't jump to line 399 because the condition on line 398 was never true
399 raise ValueError(f"Parameter 'totalDuration' can not be less than test durations.")
400 else: # no setup, no teardown, no total
401 totalDuration = testDuration
402 # no test
403 elif totalDuration is not None:
404 testDuration = totalDuration
405 if setupDuration is not None: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true
406 testDuration -= setupDuration
407 if teardownDuration is not None: 407 ↛ 408line 407 didn't jump to line 408 because the condition on line 407 was never true
408 testDuration -= teardownDuration
410 self._startTime = startTime
411 self._setupDuration = setupDuration
412 self._testDuration = testDuration
413 self._teardownDuration = teardownDuration
414 self._totalDuration = totalDuration
416 if not isinstance(warningCount, int): 416 ↛ 417line 416 didn't jump to line 417 because the condition on line 416 was never true
417 ex = TypeError(f"Parameter 'warningCount' is not of type 'int'.")
418 if version_info >= (3, 11): # pragma: no cover
419 ex.add_note(f"Got type '{getFullyQualifiedName(warningCount)}'.")
420 raise ex
422 if not isinstance(errorCount, int): 422 ↛ 423line 422 didn't jump to line 423 because the condition on line 422 was never true
423 ex = TypeError(f"Parameter 'errorCount' is not of type 'int'.")
424 if version_info >= (3, 11): # pragma: no cover
425 ex.add_note(f"Got type '{getFullyQualifiedName(errorCount)}'.")
426 raise ex
428 if not isinstance(fatalCount, int): 428 ↛ 429line 428 didn't jump to line 429 because the condition on line 428 was never true
429 ex = TypeError(f"Parameter 'fatalCount' is not of type 'int'.")
430 if version_info >= (3, 11): # pragma: no cover
431 ex.add_note(f"Got type '{getFullyQualifiedName(fatalCount)}'.")
432 raise ex
434 self._warningCount = warningCount
435 self._errorCount = errorCount
436 self._fatalCount = fatalCount
438 if keyValuePairs is not None and not isinstance(keyValuePairs, Mapping): 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true
439 ex = TypeError(f"Parameter 'keyValuePairs' is not a mapping.")
440 if version_info >= (3, 11): # pragma: no cover
441 ex.add_note(f"Got type '{getFullyQualifiedName(keyValuePairs)}'.")
442 raise ex
444 self._dict = {} if keyValuePairs is None else {k: v for k, v in keyValuePairs}
446 # QUESTION: allow Parent as setter?
447 @readonly
448 def Parent(self) -> Nullable["TestsuiteBase"]:
449 """
450 Read-only property returning the reference to the parent test entity.
452 :return: Reference to the parent entity.
453 """
454 return self._parent
456 @readonly
457 def Name(self) -> str:
458 """
459 Read-only property returning the test entity's name.
461 :return:
462 """
463 return self._name
465 @readonly
466 def StartTime(self) -> Nullable[datetime]:
467 """
468 Read-only property returning the time when the test entity was started.
470 :return: Time when the test entity was started.
471 """
472 return self._startTime
474 @readonly
475 def SetupDuration(self) -> Nullable[timedelta]:
476 """
477 Read-only property returning the duration of the test entity's setup.
479 :return: Duration it took to set up the entity.
480 """
481 return self._setupDuration
483 @readonly
484 def TestDuration(self) -> Nullable[timedelta]:
485 """
486 Read-only property returning the duration of a test entities run.
488 This duration is excluding setup and teardown durations. In case setup and/or teardown durations are unknown or not
489 distinguishable, assign setup and teardown durations with zero.
491 :return: Duration of the entity's test run.
492 """
493 return self._testDuration
495 @readonly
496 def TeardownDuration(self) -> Nullable[timedelta]:
497 """
498 Read-only property returning the duration of the test entity's teardown.
500 :return: Duration it took to tear down the entity.
501 """
502 return self._teardownDuration
504 @readonly
505 def TotalDuration(self) -> Nullable[timedelta]:
506 """
507 Read-only property returning the total duration of a test entity run.
509 this duration includes setup and teardown durations.
511 :return: Total duration of the entity's execution (setup + test + teardown)
512 """
513 return self._totalDuration
515 @readonly
516 def WarningCount(self) -> int:
517 """
518 Read-only property returning the number of encountered warnings.
520 :return: Count of encountered warnings.
521 """
522 return self._warningCount
524 @readonly
525 def ErrorCount(self) -> int:
526 """
527 Read-only property returning the number of encountered errors.
529 :return: Count of encountered errors.
530 """
531 return self._errorCount
533 @readonly
534 def FatalCount(self) -> int:
535 """
536 Read-only property returning the number of encountered fatal errors.
538 :return: Count of encountered fatal errors.
539 """
540 return self._fatalCount
542 def __len__(self) -> int:
543 """
544 Returns the number of annotated key-value pairs.
546 :return: Number of annotated key-value pairs.
547 """
548 return len(self._dict)
550 def __getitem__(self, key: str) -> Any:
551 """
552 Access a key-value pair by key.
554 :param key: Name if the key-value pair.
555 :return: Value of the accessed key.
556 """
557 return self._dict[key]
559 def __setitem__(self, key: str, value: Any) -> None:
560 """
561 Set the value of a key-value pair by key.
563 If the pair doesn't exist yet, it's created.
565 :param key: Key of the key-value pair.
566 :param value: Value of the key-value pair.
567 """
568 self._dict[key] = value
570 def __delitem__(self, key: str) -> None:
571 """
572 Delete a key-value pair by key.
574 :param key: Name if the key-value pair.
575 """
576 del self._dict[key]
578 def __contains__(self, key: str) -> bool:
579 """
580 Returns True, if a key-value pairs was annotated by this key.
582 :param key: Name of the key-value pair.
583 :return: True, if the pair was annotated.
584 """
585 return key in self._dict
587 def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
588 """
589 Iterate all annotated key-value pairs.
591 :return: A generator of key-value pair tuples (key, value).
592 """
593 yield from self._dict.items()
595 @abstractmethod
596 def Aggregate(self, strict: bool = True):
597 """
598 Aggregate all test entities in the hierarchy.
600 :return:
601 """
603 @abstractmethod
604 def __str__(self) -> str:
605 """
606 Formats the test entity as human-readable incl. some statistics.
607 """
610@export
611class Testcase(Base):
612 """
613 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run.
615 Test cases are grouped by test suites in the test entity hierarchy. The root of the hierarchy is a test summary.
617 Every test case has an overall status like unknown, skipped, failed or passed.
619 In addition to all features from its base-class, test cases provide additional statistics for passed and failed
620 assertions (checks) as well as a sum thereof.
621 """
623 _status: TestcaseStatus
624 _assertionCount: Nullable[int]
625 _failedAssertionCount: Nullable[int]
626 _passedAssertionCount: Nullable[int]
628 def __init__(
629 self,
630 name: str,
631 startTime: Nullable[datetime] = None,
632 setupDuration: Nullable[timedelta] = None,
633 testDuration: Nullable[timedelta] = None,
634 teardownDuration: Nullable[timedelta] = None,
635 totalDuration: Nullable[timedelta] = None,
636 status: TestcaseStatus = TestcaseStatus.Unknown,
637 assertionCount: Nullable[int] = None,
638 failedAssertionCount: Nullable[int] = None,
639 passedAssertionCount: Nullable[int] = None,
640 warningCount: int = 0,
641 errorCount: int = 0,
642 fatalCount: int = 0,
643 keyValuePairs: Nullable[Mapping[str, Any]] = None,
644 parent: Nullable["Testsuite"] = None
645 ):
646 """
647 Initializes the fields of a test case.
649 :param name: Name of the test entity.
650 :param startTime: Time when the test entity was started.
651 :param setupDuration: Duration it took to set up the entity.
652 :param testDuration: Duration of the entity's test run.
653 :param teardownDuration: Duration it took to tear down the entity.
654 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
655 :param status: Status of the test case.
656 :param assertionCount: Number of assertions within the test.
657 :param failedAssertionCount: Number of failed assertions within the test.
658 :param passedAssertionCount: Number of passed assertions within the test.
659 :param warningCount: Count of encountered warnings.
660 :param errorCount: Count of encountered errors.
661 :param fatalCount: Count of encountered fatal errors.
662 :param keyValuePairs: Mapping of key-value pairs to initialize the test case.
663 :param parent: Reference to the parent test suite.
664 :raises TypeError: If parameter 'parent' is not a Testsuite.
665 :raises ValueError: If parameter 'assertionCount' is not consistent.
666 """
668 if parent is not None:
669 if not isinstance(parent, Testsuite):
670 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.")
671 if version_info >= (3, 11): # pragma: no cover
672 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
673 raise ex
675 parent._testcases[name] = self
677 super().__init__(
678 name,
679 startTime,
680 setupDuration,
681 testDuration,
682 teardownDuration,
683 totalDuration,
684 warningCount,
685 errorCount,
686 fatalCount,
687 keyValuePairs,
688 parent
689 )
691 if not isinstance(status, TestcaseStatus): 691 ↛ 692line 691 didn't jump to line 692 because the condition on line 691 was never true
692 ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.")
693 if version_info >= (3, 11): # pragma: no cover
694 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.")
695 raise ex
697 self._status = status
699 if assertionCount is not None and not isinstance(assertionCount, int): 699 ↛ 700line 699 didn't jump to line 700 because the condition on line 699 was never true
700 ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.")
701 if version_info >= (3, 11): # pragma: no cover
702 ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.")
703 raise ex
705 if failedAssertionCount is not None and not isinstance(failedAssertionCount, int): 705 ↛ 706line 705 didn't jump to line 706 because the condition on line 705 was never true
706 ex = TypeError(f"Parameter 'failedAssertionCount' is not of type 'int'.")
707 if version_info >= (3, 11): # pragma: no cover
708 ex.add_note(f"Got type '{getFullyQualifiedName(failedAssertionCount)}'.")
709 raise ex
711 if passedAssertionCount is not None and not isinstance(passedAssertionCount, int): 711 ↛ 712line 711 didn't jump to line 712 because the condition on line 711 was never true
712 ex = TypeError(f"Parameter 'passedAssertionCount' is not of type 'int'.")
713 if version_info >= (3, 11): # pragma: no cover
714 ex.add_note(f"Got type '{getFullyQualifiedName(passedAssertionCount)}'.")
715 raise ex
717 self._assertionCount = assertionCount
718 if assertionCount is not None:
719 if failedAssertionCount is not None:
720 self._failedAssertionCount = failedAssertionCount
722 if passedAssertionCount is not None:
723 if passedAssertionCount + failedAssertionCount != assertionCount:
724 raise ValueError(f"passed assertion count ({passedAssertionCount}) + failed assertion count ({failedAssertionCount} != assertion count ({assertionCount}")
726 self._passedAssertionCount = passedAssertionCount
727 else:
728 self._passedAssertionCount = assertionCount - failedAssertionCount
729 elif passedAssertionCount is not None:
730 self._passedAssertionCount = passedAssertionCount
731 self._failedAssertionCount = assertionCount - passedAssertionCount
732 else:
733 raise ValueError(f"Neither passed assertion count nor failed assertion count are provided.")
734 elif failedAssertionCount is not None:
735 self._failedAssertionCount = failedAssertionCount
737 if passedAssertionCount is not None:
738 self._passedAssertionCount = passedAssertionCount
739 self._assertionCount = passedAssertionCount + failedAssertionCount
740 else:
741 raise ValueError(f"Passed assertion count is mandatory, if failed assertion count is provided instead of assertion count.")
742 elif passedAssertionCount is not None:
743 raise ValueError(f"Assertion count or failed assertion count is mandatory, if passed assertion count is provided.")
744 else:
745 self._passedAssertionCount = None
746 self._failedAssertionCount = None
748 @readonly
749 def Status(self) -> TestcaseStatus:
750 """
751 Read-only property returning the status of the test case.
753 :return: The test case's status.
754 """
755 return self._status
757 @readonly
758 def AssertionCount(self) -> int:
759 """
760 Read-only property returning the number of assertions (checks) in a test case.
762 :return: Number of assertions.
763 """
764 if self._assertionCount is None:
765 return 0
766 return self._assertionCount
768 @readonly
769 def FailedAssertionCount(self) -> int:
770 """
771 Read-only property returning the number of failed assertions (failed checks) in a test case.
773 :return: Number of assertions.
774 """
775 return self._failedAssertionCount
777 @readonly
778 def PassedAssertionCount(self) -> int:
779 """
780 Read-only property returning the number of passed assertions (successful checks) in a test case.
782 :return: Number of passed assertions.
783 """
784 return self._passedAssertionCount
786 def Copy(self) -> "Testcase":
787 return self.__class__(
788 self._name,
789 self._startTime,
790 self._setupDuration,
791 self._testDuration,
792 self._teardownDuration,
793 self._totalDuration,
794 self._status,
795 self._warningCount,
796 self._errorCount,
797 self._fatalCount,
798 )
800 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType:
801 if self._status is TestcaseStatus.Unknown: 801 ↛ 826line 801 didn't jump to line 826 because the condition on line 801 was always true
802 if self._assertionCount is None: 802 ↛ 803line 802 didn't jump to line 803 because the condition on line 802 was never true
803 self._status = TestcaseStatus.Passed
804 elif self._assertionCount == 0: 804 ↛ 805line 804 didn't jump to line 805 because the condition on line 804 was never true
805 self._status = TestcaseStatus.Weak
806 elif self._failedAssertionCount == 0:
807 self._status = TestcaseStatus.Passed
808 else:
809 self._status = TestcaseStatus.Failed
811 if self._warningCount > 0: 811 ↛ 812line 811 didn't jump to line 812 because the condition on line 811 was never true
812 self._status |= TestcaseStatus.Warned
814 if self._errorCount > 0: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true
815 self._status |= TestcaseStatus.Errored
817 if self._fatalCount > 0: 817 ↛ 818line 817 didn't jump to line 818 because the condition on line 817 was never true
818 self._status |= TestcaseStatus.Aborted
820 if strict:
821 self._status = self._status & ~TestcaseStatus.Passed | TestcaseStatus.Failed
823 # TODO: check for setup errors
824 # TODO: check for teardown errors
826 totalDuration = timedelta() if self._totalDuration is None else self._totalDuration
828 return self._warningCount, self._errorCount, self._fatalCount, totalDuration
830 def __str__(self) -> str:
831 """
832 Formats the test case as human-readable incl. statistics.
834 :pycode:`f"<Testcase {}: {} - assert/pass/fail:{}/{}/{} - warn/error/fatal:{}/{}/{} - setup/test/teardown:{}/{}/{}>"`
836 :return: Human-readable summary of a test case object.
837 """
838 return (
839 f"<Testcase {self._name}: {self._status.name} -"
840 f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
841 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount} -"
842 f" setup/test/teardown:{self._setupDuration:.3f}/{self._testDuration:.3f}/{self._teardownDuration:.3f}>"
843 )
846@export
847class TestsuiteBase(Base, Generic[TestsuiteType]):
848 """
849 Base-class for all test suites and for test summaries.
851 A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root
852 element in that hierarchy. While a test suite groups other test suites and test cases, a test summary can only group
853 test suites. Thus, a test summary contains no test cases.
854 """
856 _kind: TestsuiteKind
857 _status: TestsuiteStatus
858 _testsuites: Dict[str, TestsuiteType]
860 _tests: int
861 _inconsistent: int
862 _excluded: int
863 _skipped: int
864 _errored: int
865 _weak: int
866 _failed: int
867 _passed: int
869 def __init__(
870 self,
871 name: str,
872 kind: TestsuiteKind = TestsuiteKind.Logical,
873 startTime: Nullable[datetime] = None,
874 setupDuration: Nullable[timedelta] = None,
875 testDuration: Nullable[timedelta] = None,
876 teardownDuration: Nullable[timedelta] = None,
877 totalDuration: Nullable[timedelta] = None,
878 status: TestsuiteStatus = TestsuiteStatus.Unknown,
879 warningCount: int = 0,
880 errorCount: int = 0,
881 fatalCount: int = 0,
882 testsuites: Nullable[Iterable[TestsuiteType]] = None,
883 keyValuePairs: Nullable[Mapping[str, Any]] = None,
884 parent: Nullable["Testsuite"] = None
885 ):
886 """
887 Initializes the based-class fields of a test suite or test summary.
889 :param name: Name of the test entity.
890 :param kind: Kind of the test entity.
891 :param startTime: Time when the test entity was started.
892 :param setupDuration: Duration it took to set up the entity.
893 :param testDuration: Duration of all tests listed in the test entity.
894 :param teardownDuration: Duration it took to tear down the entity.
895 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
896 :param status: Overall status of the test entity.
897 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
898 :param errorCount: Count of encountered errors incl. errors from sub-elements.
899 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
900 :param testsuites: List of test suites to initialize the test entity with.
901 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with.
902 :param parent: Reference to the parent test entity.
903 :raises TypeError: If parameter 'parent' is not a TestsuiteBase.
904 :raises TypeError: If parameter 'testsuites' is not iterable.
905 :raises TypeError: If element in parameter 'testsuites' is not a Testsuite.
906 :raises AlreadyInHierarchyException: If a test suite in parameter 'testsuites' is already part of a test entity hierarchy.
907 :raises DuplicateTestsuiteException: If a test suite in parameter 'testsuites' is already listed (by name) in the list of test suites.
908 """
909 if parent is not None:
910 if not isinstance(parent, TestsuiteBase): 910 ↛ 911line 910 didn't jump to line 911 because the condition on line 910 was never true
911 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
912 if version_info >= (3, 11): # pragma: no cover
913 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
914 raise ex
916 parent._testsuites[name] = self
918 super().__init__(
919 name,
920 startTime,
921 setupDuration,
922 testDuration,
923 teardownDuration,
924 totalDuration,
925 warningCount,
926 errorCount,
927 fatalCount,
928 keyValuePairs,
929 parent
930 )
932 self._kind = kind
933 self._status = status
935 self._testsuites = {}
936 if testsuites is not None:
937 if not isinstance(testsuites, Iterable): 937 ↛ 938line 937 didn't jump to line 938 because the condition on line 937 was never true
938 ex = TypeError(f"Parameter 'testsuites' is not iterable.")
939 if version_info >= (3, 11): # pragma: no cover
940 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.")
941 raise ex
943 for testsuite in testsuites:
944 if not isinstance(testsuite, Testsuite): 944 ↛ 945line 944 didn't jump to line 945 because the condition on line 944 was never true
945 ex = TypeError(f"Element of parameter 'testsuites' is not of type 'Testsuite'.")
946 if version_info >= (3, 11): # pragma: no cover
947 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.")
948 raise ex
950 if testsuite._parent is not None: 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true
951 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
953 if testsuite._name in self._testsuites:
954 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
956 testsuite._parent = self
957 self._testsuites[testsuite._name] = testsuite
959 self._status = TestsuiteStatus.Unknown
960 self._tests = 0
961 self._inconsistent = 0
962 self._excluded = 0
963 self._skipped = 0
964 self._errored = 0
965 self._weak = 0
966 self._failed = 0
967 self._passed = 0
969 @readonly
970 def Kind(self) -> TestsuiteKind:
971 """
972 Read-only property returning the kind of the test suite.
974 Test suites are used to group test cases. This grouping can be due to language/framework specifics like tests
975 grouped by a module file or namespace. Others might be just logically grouped without any relation to a programming
976 language construct.
978 Test summaries always return kind ``Root``.
980 :return: Kind of the test suite.
981 """
982 return self._kind
984 @readonly
985 def Status(self) -> TestsuiteStatus:
986 """
987 Read-only property returning the aggregated overall status of the test suite.
989 :return: Overall status of the test suite.
990 """
991 return self._status
993 @readonly
994 def Testsuites(self) -> Dict[str, TestsuiteType]:
995 """
996 Read-only property returning a reference to the internal dictionary of test suites.
998 :return: Reference to the dictionary of test suite.
999 """
1000 return self._testsuites
1002 @readonly
1003 def TestsuiteCount(self) -> int:
1004 """
1005 Read-only property returning the number of all test suites in the test suite hierarchy.
1007 :return: Number of test suites.
1008 """
1009 return 1 + sum(testsuite.TestsuiteCount for testsuite in self._testsuites.values())
1011 @readonly
1012 def TestcaseCount(self) -> int:
1013 """
1014 Read-only property returning the number of all test cases in the test entity hierarchy.
1016 :return: Number of test cases.
1017 """
1018 return sum(testsuite.TestcaseCount for testsuite in self._testsuites.values())
1020 @readonly
1021 def AssertionCount(self) -> int:
1022 """
1023 Read-only property returning the number of all assertions in all test cases in the test entity hierarchy.
1025 :return: Number of assertions in all test cases.
1026 """
1027 return sum(ts.AssertionCount for ts in self._testsuites.values())
1029 @readonly
1030 def FailedAssertionCount(self) -> int:
1031 """
1032 Read-only property returning the number of all failed assertions in all test cases in the test entity hierarchy.
1034 :return: Number of failed assertions in all test cases.
1035 """
1036 raise NotImplementedError()
1037 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount)
1039 @readonly
1040 def PassedAssertionCount(self) -> int:
1041 """
1042 Read-only property returning the number of all passed assertions in all test cases in the test entity hierarchy.
1044 :return: Number of passed assertions in all test cases.
1045 """
1046 raise NotImplementedError()
1047 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount)
1049 @readonly
1050 def Tests(self) -> int:
1051 return self._tests
1053 @readonly
1054 def Inconsistent(self) -> int:
1055 """
1056 Read-only property returning the number of inconsistent tests in the test suite hierarchy.
1058 :return: Number of inconsistent tests.
1059 """
1060 return self._inconsistent
1062 @readonly
1063 def Excluded(self) -> int:
1064 """
1065 Read-only property returning the number of excluded tests in the test suite hierarchy.
1067 :return: Number of excluded tests.
1068 """
1069 return self._excluded
1071 @readonly
1072 def Skipped(self) -> int:
1073 """
1074 Read-only property returning the number of skipped tests in the test suite hierarchy.
1076 :return: Number of skipped tests.
1077 """
1078 return self._skipped
1080 @readonly
1081 def Errored(self) -> int:
1082 """
1083 Read-only property returning the number of tests with errors in the test suite hierarchy.
1085 :return: Number of errored tests.
1086 """
1087 return self._errored
1089 @readonly
1090 def Weak(self) -> int:
1091 """
1092 Read-only property returning the number of weak tests in the test suite hierarchy.
1094 :return: Number of weak tests.
1095 """
1096 return self._weak
1098 @readonly
1099 def Failed(self) -> int:
1100 """
1101 Read-only property returning the number of failed tests in the test suite hierarchy.
1103 :return: Number of failed tests.
1104 """
1105 return self._failed
1107 @readonly
1108 def Passed(self) -> int:
1109 """
1110 Read-only property returning the number of passed tests in the test suite hierarchy.
1112 :return: Number of passed tests.
1113 """
1114 return self._passed
1116 @readonly
1117 def WarningCount(self) -> int:
1118 raise NotImplementedError()
1119 # return self._warningCount
1121 @readonly
1122 def ErrorCount(self) -> int:
1123 raise NotImplementedError()
1124 # return self._errorCount
1126 @readonly
1127 def FatalCount(self) -> int:
1128 raise NotImplementedError()
1129 # return self._fatalCount
1131 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1132 tests = 0
1133 inconsistent = 0
1134 excluded = 0
1135 skipped = 0
1136 errored = 0
1137 weak = 0
1138 failed = 0
1139 passed = 0
1141 warningCount = 0
1142 errorCount = 0
1143 fatalCount = 0
1145 totalDuration = timedelta()
1147 for testsuite in self._testsuites.values():
1148 t, i, ex, s, e, w, f, p, wc, ec, fc, td = testsuite.Aggregate(strict)
1149 tests += t
1150 inconsistent += i
1151 excluded += ex
1152 skipped += s
1153 errored += e
1154 weak += w
1155 failed += f
1156 passed += p
1158 warningCount += wc
1159 errorCount += ec
1160 fatalCount += fc
1162 totalDuration += td
1164 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration
1166 def AddTestsuite(self, testsuite: TestsuiteType) -> None:
1167 """
1168 Add a test suite to the list of test suites.
1170 :param testsuite: The test suite to add.
1171 :raises ValueError: If parameter 'testsuite' is None.
1172 :raises TypeError: If parameter 'testsuite' is not a Testsuite.
1173 :raises AlreadyInHierarchyException: If parameter 'testsuite' is already part of a test entity hierarchy.
1174 :raises DuplicateTestcaseException: If parameter 'testsuite' is already listed (by name) in the list of test suites.
1175 """
1176 if testsuite is None: 1176 ↛ 1177line 1176 didn't jump to line 1177 because the condition on line 1176 was never true
1177 raise ValueError("Parameter 'testsuite' is None.")
1178 elif not isinstance(testsuite, Testsuite): 1178 ↛ 1179line 1178 didn't jump to line 1179 because the condition on line 1178 was never true
1179 ex = TypeError(f"Parameter 'testsuite' is not of type 'Testsuite'.")
1180 if version_info >= (3, 11): # pragma: no cover
1181 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.")
1182 raise ex
1184 if testsuite._parent is not None: 1184 ↛ 1185line 1184 didn't jump to line 1185 because the condition on line 1184 was never true
1185 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1187 if testsuite._name in self._testsuites:
1188 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1190 testsuite._parent = self
1191 self._testsuites[testsuite._name] = testsuite
1193 def AddTestsuites(self, testsuites: Iterable[TestsuiteType]) -> None:
1194 """
1195 Add a list of test suites to the list of test suites.
1197 :param testsuites: List of test suites to add.
1198 :raises ValueError: If parameter 'testsuites' is None.
1199 :raises TypeError: If parameter 'testsuites' is not iterable.
1200 """
1201 if testsuites is None: 1201 ↛ 1202line 1201 didn't jump to line 1202 because the condition on line 1201 was never true
1202 raise ValueError("Parameter 'testsuites' is None.")
1203 elif not isinstance(testsuites, Iterable): 1203 ↛ 1204line 1203 didn't jump to line 1204 because the condition on line 1203 was never true
1204 ex = TypeError(f"Parameter 'testsuites' is not iterable.")
1205 if version_info >= (3, 11): # pragma: no cover
1206 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.")
1207 raise ex
1209 for testsuite in testsuites:
1210 self.AddTestsuite(testsuite)
1212 @abstractmethod
1213 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1214 pass
1216 def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
1217 return self.Iterate(scheme)
1219 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
1220 return self.Iterate(scheme)
1222 def ToTree(self) -> Node:
1223 rootNode = Node(value=self._name)
1225 def convertTestcase(testcase: Testcase, parentNode: Node) -> None:
1226 _ = Node(value=testcase._name, parent=parentNode)
1228 def convertTestsuite(testsuite: Testsuite, parentNode: Node) -> None:
1229 testsuiteNode = Node(value=testsuite._name, parent=parentNode)
1231 for ts in testsuite._testsuites.values():
1232 convertTestsuite(ts, testsuiteNode)
1234 for tc in testsuite._testcases.values():
1235 convertTestcase(tc, testsuiteNode)
1237 for testsuite in self._testsuites.values():
1238 convertTestsuite(testsuite, rootNode)
1240 return rootNode
1243@export
1244class Testsuite(TestsuiteBase[TestsuiteType]):
1245 """
1246 A testsuite is a mid-level element in the test entity hierarchy representing a group of tests.
1248 Test suites contain test cases and optionally other test suites. Test suites can be grouped by test suites to form a
1249 hierarchy of test entities. The root of the hierarchy is a test summary.
1250 """
1252 _testcases: Dict[str, "Testcase"]
1254 def __init__(
1255 self,
1256 name: str,
1257 kind: TestsuiteKind = TestsuiteKind.Logical,
1258 startTime: Nullable[datetime] = None,
1259 setupDuration: Nullable[timedelta] = None,
1260 testDuration: Nullable[timedelta] = None,
1261 teardownDuration: Nullable[timedelta] = None,
1262 totalDuration: Nullable[timedelta] = None,
1263 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1264 warningCount: int = 0,
1265 errorCount: int = 0,
1266 fatalCount: int = 0,
1267 testsuites: Nullable[Iterable[TestsuiteType]] = None,
1268 testcases: Nullable[Iterable["Testcase"]] = None,
1269 keyValuePairs: Nullable[Mapping[str, Any]] = None,
1270 parent: Nullable[TestsuiteType] = None
1271 ):
1272 """
1273 Initializes the fields of a test suite.
1275 :param name: Name of the test suite.
1276 :param kind: Kind of the test suite.
1277 :param startTime: Time when the test suite was started.
1278 :param setupDuration: Duration it took to set up the test suite.
1279 :param testDuration: Duration of all tests listed in the test suite.
1280 :param teardownDuration: Duration it took to tear down the test suite.
1281 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
1282 :param status: Overall status of the test suite.
1283 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
1284 :param errorCount: Count of encountered errors incl. errors from sub-elements.
1285 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
1286 :param testsuites: List of test suites to initialize the test suite with.
1287 :param testcases: List of test cases to initialize the test suite with.
1288 :param keyValuePairs: Mapping of key-value pairs to initialize the test suite with.
1289 :param parent: Reference to the parent test entity.
1290 :raises TypeError: If parameter 'testcases' is not iterable.
1291 :raises TypeError: If element in parameter 'testcases' is not a Testcase.
1292 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy.
1293 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases.
1294 """
1295 super().__init__(
1296 name,
1297 kind,
1298 startTime,
1299 setupDuration,
1300 testDuration,
1301 teardownDuration,
1302 totalDuration,
1303 status,
1304 warningCount,
1305 errorCount,
1306 fatalCount,
1307 testsuites,
1308 keyValuePairs,
1309 parent
1310 )
1312 # self._testDuration = testDuration
1314 self._testcases = {}
1315 if testcases is not None:
1316 if not isinstance(testcases, Iterable): 1316 ↛ 1317line 1316 didn't jump to line 1317 because the condition on line 1316 was never true
1317 ex = TypeError(f"Parameter 'testcases' is not iterable.")
1318 if version_info >= (3, 11): # pragma: no cover
1319 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.")
1320 raise ex
1322 for testcase in testcases:
1323 if not isinstance(testcase, Testcase): 1323 ↛ 1324line 1323 didn't jump to line 1324 because the condition on line 1323 was never true
1324 ex = TypeError(f"Element of parameter 'testcases' is not of type 'Testcase'.")
1325 if version_info >= (3, 11): # pragma: no cover
1326 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.")
1327 raise ex
1329 if testcase._parent is not None: 1329 ↛ 1330line 1329 didn't jump to line 1330 because the condition on line 1329 was never true
1330 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
1332 if testcase._name in self._testcases:
1333 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1335 testcase._parent = self
1336 self._testcases[testcase._name] = testcase
1338 @readonly
1339 def Testcases(self) -> Dict[str, "Testcase"]:
1340 """
1341 Read-only property returning a reference to the internal dictionary of test cases.
1343 :return: Reference to the dictionary of test cases.
1344 """
1345 return self._testcases
1347 @readonly
1348 def TestcaseCount(self) -> int:
1349 """
1350 Read-only property returning the number of all test cases in the test entity hierarchy.
1352 :return: Number of test cases.
1353 """
1354 return super().TestcaseCount + len(self._testcases)
1356 @readonly
1357 def AssertionCount(self) -> int:
1358 return super().AssertionCount + sum(tc.AssertionCount for tc in self._testcases.values())
1360 def Copy(self) -> "Testsuite":
1361 return self.__class__(
1362 self._name,
1363 self._startTime,
1364 self._setupDuration,
1365 self._teardownDuration,
1366 self._totalDuration,
1367 self._status,
1368 self._warningCount,
1369 self._errorCount,
1370 self._fatalCount
1371 )
1373 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1374 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration = super().Aggregate()
1376 for testcase in self._testcases.values():
1377 wc, ec, fc, td = testcase.Aggregate(strict)
1379 tests += 1
1381 warningCount += wc
1382 errorCount += ec
1383 fatalCount += fc
1385 totalDuration += td
1387 status = testcase._status
1388 if status is TestcaseStatus.Unknown: 1388 ↛ 1389line 1388 didn't jump to line 1389 because the condition on line 1388 was never true
1389 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.")
1390 elif TestcaseStatus.Inconsistent in status: 1390 ↛ 1391line 1390 didn't jump to line 1391 because the condition on line 1390 was never true
1391 inconsistent += 1
1392 elif status is TestcaseStatus.Excluded: 1392 ↛ 1393line 1392 didn't jump to line 1393 because the condition on line 1392 was never true
1393 excluded += 1
1394 elif status is TestcaseStatus.Skipped:
1395 skipped += 1
1396 elif status is TestcaseStatus.Errored: 1396 ↛ 1397line 1396 didn't jump to line 1397 because the condition on line 1396 was never true
1397 errored += 1
1398 elif status is TestcaseStatus.Weak: 1398 ↛ 1399line 1398 didn't jump to line 1399 because the condition on line 1398 was never true
1399 weak += 1
1400 elif status is TestcaseStatus.Passed:
1401 passed += 1
1402 elif status is TestcaseStatus.Failed: 1402 ↛ 1404line 1402 didn't jump to line 1404 because the condition on line 1402 was always true
1403 failed += 1
1404 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown:
1405 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.")
1406 else:
1407 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.")
1409 self._tests = tests
1410 self._inconsistent = inconsistent
1411 self._excluded = excluded
1412 self._skipped = skipped
1413 self._errored = errored
1414 self._weak = weak
1415 self._failed = failed
1416 self._passed = passed
1418 self._warningCount = warningCount
1419 self._errorCount = errorCount
1420 self._fatalCount = fatalCount
1422 if self._totalDuration is None:
1423 self._totalDuration = totalDuration
1425 if errored > 0: 1425 ↛ 1426line 1425 didn't jump to line 1426 because the condition on line 1425 was never true
1426 self._status = TestsuiteStatus.Errored
1427 elif failed > 0:
1428 self._status = TestsuiteStatus.Failed
1429 elif tests == 0: 1429 ↛ 1430line 1429 didn't jump to line 1430 because the condition on line 1429 was never true
1430 self._status = TestsuiteStatus.Empty
1431 elif tests - skipped == passed: 1431 ↛ 1433line 1431 didn't jump to line 1433 because the condition on line 1431 was always true
1432 self._status = TestsuiteStatus.Passed
1433 elif tests == skipped:
1434 self._status = TestsuiteStatus.Skipped
1435 else:
1436 self._status = TestsuiteStatus.Unknown
1438 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration
1440 def AddTestcase(self, testcase: "Testcase") -> None:
1441 """
1442 Add a test case to the list of test cases.
1444 :param testcase: The test case to add.
1445 :raises ValueError: If parameter 'testcase' is None.
1446 :raises TypeError: If parameter 'testcase' is not a Testcase.
1447 :raises AlreadyInHierarchyException: If parameter 'testcase' is already part of a test entity hierarchy.
1448 :raises DuplicateTestcaseException: If parameter 'testcase' is already listed (by name) in the list of test cases.
1449 """
1450 if testcase is None: 1450 ↛ 1451line 1450 didn't jump to line 1451 because the condition on line 1450 was never true
1451 raise ValueError("Parameter 'testcase' is None.")
1452 elif not isinstance(testcase, Testcase): 1452 ↛ 1453line 1452 didn't jump to line 1453 because the condition on line 1452 was never true
1453 ex = TypeError(f"Parameter 'testcase' is not of type 'Testcase'.")
1454 if version_info >= (3, 11): # pragma: no cover
1455 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.")
1456 raise ex
1458 if testcase._parent is not None: 1458 ↛ 1459line 1458 didn't jump to line 1459 because the condition on line 1458 was never true
1459 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
1461 if testcase._name in self._testcases:
1462 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1464 testcase._parent = self
1465 self._testcases[testcase._name] = testcase
1467 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None:
1468 """
1469 Add a list of test cases to the list of test cases.
1471 :param testcases: List of test cases to add.
1472 :raises ValueError: If parameter 'testcases' is None.
1473 :raises TypeError: If parameter 'testcases' is not iterable.
1474 """
1475 if testcases is None: 1475 ↛ 1476line 1475 didn't jump to line 1476 because the condition on line 1475 was never true
1476 raise ValueError("Parameter 'testcases' is None.")
1477 elif not isinstance(testcases, Iterable): 1477 ↛ 1478line 1477 didn't jump to line 1478 because the condition on line 1477 was never true
1478 ex = TypeError(f"Parameter 'testcases' is not iterable.")
1479 if version_info >= (3, 11): # pragma: no cover
1480 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.")
1481 raise ex
1483 for testcase in testcases:
1484 self.AddTestcase(testcase)
1486 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1487 assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme
1489 if IterationScheme.PreOrder in scheme:
1490 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1491 yield self
1493 if IterationScheme.IncludeTestcases in scheme:
1494 for testcase in self._testcases.values():
1495 yield testcase
1497 for testsuite in self._testsuites.values():
1498 yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf)
1500 if IterationScheme.PostOrder in scheme:
1501 if IterationScheme.IncludeTestcases in scheme:
1502 for testcase in self._testcases.values():
1503 yield testcase
1505 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1506 yield self
1508 def __str__(self) -> str:
1509 return (
1510 f"<Testsuite {self._name}: {self._status.name} -"
1511 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
1512 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>"
1513 )
1516@export
1517class TestsuiteSummary(TestsuiteBase[TestsuiteType]):
1518 """
1519 A testsuite summary is the root element in the test entity hierarchy representing a summary of all test suites and cases.
1521 The testsuite summary contains test suites, which in turn can contain test suites and test cases.
1522 """
1524 def __init__(
1525 self,
1526 name: str,
1527 startTime: Nullable[datetime] = None,
1528 setupDuration: Nullable[timedelta] = None,
1529 testDuration: Nullable[timedelta] = None,
1530 teardownDuration: Nullable[timedelta] = None,
1531 totalDuration: Nullable[timedelta] = None,
1532 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1533 warningCount: int = 0,
1534 errorCount: int = 0,
1535 fatalCount: int = 0,
1536 testsuites: Nullable[Iterable[TestsuiteType]] = None,
1537 keyValuePairs: Nullable[Mapping[str, Any]] = None,
1538 parent: Nullable[TestsuiteType] = None
1539 ):
1540 """
1541 Initializes the fields of a test summary.
1543 :param name: Name of the test summary.
1544 :param startTime: Time when the test summary was started.
1545 :param setupDuration: Duration it took to set up the test summary.
1546 :param testDuration: Duration of all tests listed in the test summary.
1547 :param teardownDuration: Duration it took to tear down the test summary.
1548 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
1549 :param status: Overall status of the test summary.
1550 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
1551 :param errorCount: Count of encountered errors incl. errors from sub-elements.
1552 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
1553 :param testsuites: List of test suites to initialize the test summary with.
1554 :param keyValuePairs: Mapping of key-value pairs to initialize the test summary with.
1555 :param parent: Reference to the parent test summary.
1556 """
1557 super().__init__(
1558 name,
1559 TestsuiteKind.Root,
1560 startTime,
1561 setupDuration,
1562 testDuration,
1563 teardownDuration,
1564 totalDuration,
1565 status,
1566 warningCount,
1567 errorCount,
1568 fatalCount,
1569 testsuites,
1570 keyValuePairs,
1571 parent
1572 )
1574 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1575 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration = super().Aggregate(strict)
1577 self._tests = tests
1578 self._inconsistent = inconsistent
1579 self._excluded = excluded
1580 self._skipped = skipped
1581 self._errored = errored
1582 self._weak = weak
1583 self._failed = failed
1584 self._passed = passed
1586 self._warningCount = warningCount
1587 self._errorCount = errorCount
1588 self._fatalCount = fatalCount
1590 if self._totalDuration is None: 1590 ↛ 1593line 1590 didn't jump to line 1593 because the condition on line 1590 was always true
1591 self._totalDuration = totalDuration
1593 if errored > 0: 1593 ↛ 1594line 1593 didn't jump to line 1594 because the condition on line 1593 was never true
1594 self._status = TestsuiteStatus.Errored
1595 elif failed > 0:
1596 self._status = TestsuiteStatus.Failed
1597 elif tests == 0: 1597 ↛ 1598line 1597 didn't jump to line 1598 because the condition on line 1597 was never true
1598 self._status = TestsuiteStatus.Empty
1599 elif tests - skipped == passed: 1599 ↛ 1601line 1599 didn't jump to line 1601 because the condition on line 1599 was always true
1600 self._status = TestsuiteStatus.Passed
1601 elif tests == skipped:
1602 self._status = TestsuiteStatus.Skipped
1603 elif tests == excluded:
1604 self._status = TestsuiteStatus.Excluded
1605 else:
1606 self._status = TestsuiteStatus.Unknown
1608 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration
1610 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1611 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1612 yield self
1614 for testsuite in self._testsuites.values():
1615 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1617 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme: 1617 ↛ 1618line 1617 didn't jump to line 1618 because the condition on line 1617 was never true
1618 yield self
1620 def __str__(self) -> str:
1621 return (
1622 f"<TestsuiteSummary {self._name}: {self._status.name} -"
1623 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
1624 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>"
1625 )
1628@export
1629class Document(metaclass=ExtendedType, mixin=True):
1630 """A mixin-class representing a unit test summary document (file)."""
1632 _path: Path
1634 _analysisDuration: float #: TODO: replace by Timer; should be timedelta?
1635 _modelConversion: float #: TODO: replace by Timer; should be timedelta?
1637 def __init__(self, reportFile: Path, analyzeAndConvert: bool = False):
1638 self._path = reportFile
1640 self._analysisDuration = -1.0
1641 self._modelConversion = -1.0
1643 if analyzeAndConvert:
1644 self.Analyze()
1645 self.Convert()
1647 @readonly
1648 def Path(self) -> Path:
1649 """
1650 Read-only property returning the path to the file of this document.
1652 :return: The document's path to the file.
1653 """
1654 return self._path
1656 @readonly
1657 def AnalysisDuration(self) -> timedelta:
1658 """
1659 Read-only property returning analysis duration.
1661 .. note::
1663 This includes usually the duration to validate and parse the file format, but it excludes the time to convert the
1664 content to the test entity hierarchy.
1666 :return: Duration to analyze the document.
1667 """
1668 return timedelta(seconds=self._analysisDuration)
1670 @readonly
1671 def ModelConversionDuration(self) -> timedelta:
1672 """
1673 Read-only property returning conversion duration.
1675 .. note::
1677 This includes usually the duration to convert the document's content to the test entity hierarchy. It might also
1678 include the duration to (re-)aggregate all states and statistics in the hierarchy.
1680 :return: Duration to convert the document.
1681 """
1682 return timedelta(seconds=self._modelConversion)
1684 @abstractmethod
1685 def Analyze(self) -> None:
1686 """Analyze and validate the document's content."""
1688 # @abstractmethod
1689 # def Write(self, path: Nullable[Path] = None, overwrite: bool = False):
1690 # pass
1692 @abstractmethod
1693 def Convert(self):
1694 """Convert the document's content to an instance of the test entity hierarchy."""
1697@export
1698class Merged(metaclass=ExtendedType, mixin=True):
1699 """A mixin-class representing a merged test entity."""
1701 _mergedCount: int
1703 def __init__(self, mergedCount: int = 1):
1704 self._mergedCount = mergedCount
1706 @readonly
1707 def MergedCount(self) -> int:
1708 return self._mergedCount
1711@export
1712class Combined(metaclass=ExtendedType, mixin=True):
1713 _combinedCount: int
1715 def __init__(self, combinedCound: int = 1):
1716 self._combinedCount = combinedCound
1718 @readonly
1719 def CombinedCount(self) -> int:
1720 return self._combinedCount
1723@export
1724class MergedTestcase(Testcase, Merged):
1725 _mergedTestcases: List[Testcase]
1727 def __init__(
1728 self,
1729 testcase: Testcase,
1730 parent: Nullable["Testsuite"] = None
1731 ):
1732 if testcase is None: 1732 ↛ 1733line 1732 didn't jump to line 1733 because the condition on line 1732 was never true
1733 raise ValueError(f"Parameter 'testcase' is None.")
1735 super().__init__(
1736 testcase._name,
1737 testcase._startTime,
1738 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration,
1739 TestcaseStatus.Unknown,
1740 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount,
1741 testcase._warningCount, testcase._errorCount, testcase._fatalCount,
1742 parent
1743 )
1744 Merged.__init__(self)
1746 self._mergedTestcases = [testcase]
1748 @readonly
1749 def Status(self) -> TestcaseStatus:
1750 if self._status is TestcaseStatus.Unknown: 1750 ↛ 1757line 1750 didn't jump to line 1757 because the condition on line 1750 was always true
1751 status = self._mergedTestcases[0]._status
1752 for mtc in self._mergedTestcases[1:]:
1753 status @= mtc._status
1755 self._status = status
1757 return self._status
1759 @readonly
1760 def SummedAssertionCount(self) -> int:
1761 return sum(tc._assertionCount for tc in self._mergedTestcases)
1763 @readonly
1764 def SummedPassedAssertionCount(self) -> int:
1765 return sum(tc._passedAssertionCount for tc in self._mergedTestcases)
1767 @readonly
1768 def SummedFailedAssertionCount(self) -> int:
1769 return sum(tc._failedAssertionCount for tc in self._mergedTestcases)
1771 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType:
1772 firstMTC = self._mergedTestcases[0]
1774 status = firstMTC._status
1775 warningCount = firstMTC._warningCount
1776 errorCount = firstMTC._errorCount
1777 fatalCount = firstMTC._fatalCount
1778 totalDuration = firstMTC._totalDuration
1780 for mtc in self._mergedTestcases[1:]:
1781 status @= mtc._status
1782 warningCount += mtc._warningCount
1783 errorCount += mtc._errorCount
1784 fatalCount += mtc._fatalCount
1786 self._status = status
1788 return warningCount, errorCount, fatalCount, totalDuration
1790 def Merge(self, tc: Testcase) -> None:
1791 self._mergedCount += 1
1793 self._mergedTestcases.append(tc)
1795 self._warningCount += tc._warningCount
1796 self._errorCount += tc._errorCount
1797 self._fatalCount += tc._fatalCount
1799 def ToTestcase(self) -> Testcase:
1800 return Testcase(
1801 self._name,
1802 self._startTime,
1803 self._setupDuration,
1804 self._testDuration,
1805 self._teardownDuration,
1806 self._totalDuration,
1807 self._status,
1808 self._assertionCount,
1809 self._failedAssertionCount,
1810 self._passedAssertionCount,
1811 self._warningCount,
1812 self._errorCount,
1813 self._fatalCount
1814 )
1817@export
1818class MergedTestsuite(Testsuite, Merged):
1819 def __init__(
1820 self,
1821 testsuite: Testsuite,
1822 addTestsuites: bool = False,
1823 addTestcases: bool = False,
1824 parent: Nullable["Testsuite"] = None
1825 ):
1826 if testsuite is None: 1826 ↛ 1827line 1826 didn't jump to line 1827 because the condition on line 1826 was never true
1827 raise ValueError(f"Parameter 'testsuite' is None.")
1829 super().__init__(
1830 testsuite._name,
1831 testsuite._kind,
1832 testsuite._startTime,
1833 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration,
1834 TestsuiteStatus.Unknown,
1835 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount,
1836 parent
1837 )
1838 Merged.__init__(self)
1840 if addTestsuites: 1840 ↛ 1845line 1840 didn't jump to line 1845 because the condition on line 1840 was always true
1841 for ts in testsuite._testsuites.values():
1842 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases)
1843 self.AddTestsuite(mergedTestsuite)
1845 if addTestcases: 1845 ↛ exitline 1845 didn't return from function '__init__' because the condition on line 1845 was always true
1846 for tc in testsuite._testcases.values():
1847 mergedTestcase = MergedTestcase(tc)
1848 self.AddTestcase(mergedTestcase)
1850 def Merge(self, testsuite: Testsuite) -> None:
1851 self._mergedCount += 1
1853 for ts in testsuite._testsuites.values():
1854 if ts._name in self._testsuites: 1854 ↛ 1857line 1854 didn't jump to line 1857 because the condition on line 1854 was always true
1855 self._testsuites[ts._name].Merge(ts)
1856 else:
1857 mergedTestsuite = MergedTestsuite(ts, addTestsuites=True, addTestcases=True)
1858 self.AddTestsuite(mergedTestsuite)
1860 for tc in testsuite._testcases.values():
1861 if tc._name in self._testcases: 1861 ↛ 1864line 1861 didn't jump to line 1864 because the condition on line 1861 was always true
1862 self._testcases[tc._name].Merge(tc)
1863 else:
1864 mergedTestcase = MergedTestcase(tc)
1865 self.AddTestcase(mergedTestcase)
1867 def ToTestsuite(self) -> Testsuite:
1868 testsuite = Testsuite(
1869 self._name,
1870 self._kind,
1871 self._startTime,
1872 self._setupDuration,
1873 self._testDuration,
1874 self._teardownDuration,
1875 self._totalDuration,
1876 self._status,
1877 self._warningCount,
1878 self._errorCount,
1879 self._fatalCount,
1880 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values()),
1881 testcases=(tc.ToTestcase() for tc in self._testcases.values())
1882 )
1884 testsuite._tests = self._tests
1885 testsuite._excluded = self._excluded
1886 testsuite._inconsistent = self._inconsistent
1887 testsuite._skipped = self._skipped
1888 testsuite._errored = self._errored
1889 testsuite._weak = self._weak
1890 testsuite._failed = self._failed
1891 testsuite._passed = self._passed
1893 return testsuite
1896@export
1897class MergedTestsuiteSummary(TestsuiteSummary, Merged):
1898 _mergedFiles: Dict[Path, TestsuiteSummary]
1900 def __init__(self, name: str) -> None:
1901 super().__init__(name)
1902 Merged.__init__(self, mergedCount=0)
1904 self._mergedFiles = {}
1906 def Merge(self, testsuiteSummary: TestsuiteSummary) -> None:
1907 # if summary.File in self._mergedFiles:
1908 # raise
1910 # FIXME: a summary is not necessarily a file
1911 self._mergedCount += 1
1912 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary
1914 for testsuite in testsuiteSummary._testsuites.values():
1915 if testsuite._name in self._testsuites:
1916 self._testsuites[testsuite._name].Merge(testsuite)
1917 else:
1918 mergedTestsuite = MergedTestsuite(testsuite, addTestsuites=True, addTestcases=True)
1919 self.AddTestsuite(mergedTestsuite)
1921 def ToTestsuiteSummary(self) -> TestsuiteSummary:
1922 testsuiteSummary = TestsuiteSummary(
1923 self._name,
1924 self._startTime,
1925 self._setupDuration,
1926 self._testDuration,
1927 self._teardownDuration,
1928 self._totalDuration,
1929 self._status,
1930 self._warningCount,
1931 self._errorCount,
1932 self._fatalCount,
1933 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values())
1934 )
1936 testsuiteSummary._tests = self._tests
1937 testsuiteSummary._excluded = self._excluded
1938 testsuiteSummary._inconsistent = self._inconsistent
1939 testsuiteSummary._skipped = self._skipped
1940 testsuiteSummary._errored = self._errored
1941 testsuiteSummary._weak = self._weak
1942 testsuiteSummary._failed = self._failed
1943 testsuiteSummary._passed = self._passed
1945 return testsuiteSummary