Coverage for pyEDAA/Reports/Unittesting/__init__.py: 77%
843 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-29 03:43 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-29 03:43 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2024-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32The pyEDAA.Reports.Unittesting package implements a hierarchy of test entities. These are test cases, test suites and a
33test summary provided as a class hierarchy. Test cases are the leaf elements in the hierarchy and abstract an
34individual test run. Test suites are used to group multiple test cases or other test suites. The root element is a test
35summary. When such a summary is stored in a file format like Ant + JUnit4 XML, a file format specific document is
36derived from a summary class.
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 typing import Optional as Nullable, Dict, Iterable, Any, Tuple, Generator, Union, List, Generic, TypeVar, Mapping
80from pyTooling.Common import getFullyQualifiedName
81from pyTooling.Decorators import export, readonly
82from pyTooling.MetaClasses import ExtendedType, abstractmethod
83from pyTooling.Tree import Node
85from pyEDAA.Reports import ReportException
88@export
89class UnittestException(ReportException):
90 """Base-exception for all unit test related exceptions."""
93@export
94class AlreadyInHierarchyException(UnittestException):
95 """
96 A unit test exception raised if the element is already part of a hierarchy.
98 This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same
99 hierarchy should occur only once in the hierarchy.
101 .. hint::
103 This is usually caused by a non-None parent reference.
104 """
107@export
108class DuplicateTestsuiteException(UnittestException):
109 """
110 A unit test exception raised on duplicate test suites (by name).
112 This exception is raised, if a child test suite with same name already exist in the test suite.
114 .. hint::
116 Test suite names need to be unique per parent element (test suite or test summary).
117 """
120@export
121class DuplicateTestcaseException(UnittestException):
122 """
123 A unit test exception raised on duplicate test cases (by name).
125 This exception is raised, if a child test case with same name already exist in the test suite.
127 .. hint::
129 Test case names need to be unique per parent element (test suite).
130 """
133@export
134class TestcaseStatus(Flag):
135 """A flag enumeration describing the status of a test case."""
136 Unknown = 0 #: Testcase status is uninitialized and therefore unknown.
137 Excluded = 1 #: Testcase was permanently excluded / disabled
138 Skipped = 2 #: Testcase was temporarily skipped (e.g. based on a condition)
139 Weak = 4 #: No assertions were recorded.
140 Passed = 8 #: A passed testcase, because all assertions were successful.
141 Failed = 16 #: A failed testcase due to at least one failed assertion.
143 Mask = Excluded | Skipped | Weak | Passed | Failed
145 Inverted = 128 #: To mark inverted results
146 UnexpectedPassed = Failed | Inverted
147 ExpectedFailed = Passed | Inverted
149 Warned = 1024 #: Runtime warning
150 Errored = 2048 #: Runtime error (mostly caught exceptions)
151 Aborted = 4096 #: Uncaught runtime exception
153 SetupError = 8192 #: Preparation / compilation error
154 TearDownError = 16384 #: Cleanup error / resource release error
155 Inconsistent = 32768 #: Dataset is inconsistent
157 Flags = Warned | Errored | Aborted | SetupError | TearDownError | Inconsistent
159 # TODO: timed out ?
160 # TODO: some passed (if merged, mixed results of passed and failed)
162 def __matmul__(self, other: "TestcaseStatus") -> "TestcaseStatus":
163 s = self & self.Mask
164 o = other & self.Mask
165 if s is self.Excluded: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 resolved = self.Excluded if o is self.Excluded else self.Unknown
167 elif s is self.Skipped:
168 resolved = self.Unknown if (o is self.Unknown) or (o is self.Excluded) else o
169 elif s is self.Weak: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 resolved = self.Weak if o is self.Weak else self.Unknown
171 elif s is self.Passed: 171 ↛ 176line 171 didn't jump to line 176 because the condition on line 171 was always true
172 if o is self.Failed: 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true
173 resolved = self.Failed
174 else:
175 resolved = self.Passed if (o is self.Skipped) or (o is self.Passed) else self.Unknown
176 elif s is self.Failed:
177 resolved = self.Failed if (o is self.Skipped) or (o is self.Passed) or (o is self.Failed) else self.Unknown
178 else:
179 resolved = self.Unknown
181 resolved |= (self & self.Flags) | (other & self.Flags)
182 return resolved
185@export
186class TestsuiteStatus(Flag):
187 """A flag enumeration describing the status of a test suite."""
188 Unknown = 0
189 Excluded = 1 #: Testcase was permanently excluded / disabled
190 Skipped = 2 #: Testcase was temporarily skipped (e.g. based on a condition)
191 Empty = 4 #: No tests in suite
192 Passed = 8 #: Passed testcase, because all assertions succeeded
193 Failed = 16 #: Failed testcase due to failing assertions
195 Mask = Excluded | Skipped | Empty | Passed | Failed
197 Inverted = 128 #: To mark inverted results
198 UnexpectedPassed = Failed | Inverted
199 ExpectedFailed = Passed | Inverted
201 Warned = 1024 #: Runtime warning
202 Errored = 2048 #: Runtime error (mostly caught exceptions)
203 Aborted = 4096 #: Uncaught runtime exception
205 SetupError = 8192 #: Preparation / compilation error
206 TearDownError = 16384 #: Cleanup error / resource release error
208 Flags = Warned | Errored | Aborted | SetupError | TearDownError
211@export
212class TestsuiteKind(IntEnum):
213 """Enumeration describing the kind of test suite."""
214 Root = 0 #: Root element of the hierarchy.
215 Logical = 1 #: Represents a logical unit.
216 Namespace = 2 #: Represents a namespace.
217 Package = 3 #: Represents a package.
218 Module = 4 #: Represents a module.
219 Class = 5 #: Represents a class.
222@export
223class IterationScheme(Flag):
224 """
225 A flag enumeration for selecting the test suite iteration scheme.
227 When a test entity hierarchy is (recursively) iterated, this iteration scheme describes how to iterate the hierarchy
228 and what elements to return as a result.
229 """
230 Unknown = 0 #: Neutral element.
231 IncludeSelf = 1 #: Also include the element itself.
232 IncludeTestsuites = 2 #: Include test suites into the result.
233 IncludeTestcases = 4 #: Include test cases into the result.
235 Recursive = 8 #: Iterate recursively.
237 PreOrder = 16 #: Iterate in pre-order (top-down: current node, then child element left-to-right).
238 PostOrder = 32 #: Iterate in pre-order (bottom-up: child element left-to-right, then current node).
240 Default = IncludeTestsuites | Recursive | IncludeTestcases | PreOrder #: Recursively iterate all test entities in pre-order.
241 TestsuiteDefault = IncludeTestsuites | Recursive | PreOrder #: Recursively iterate only test suites in pre-order.
242 TestcaseDefault = IncludeTestcases | Recursive | PreOrder #: Recursively iterate only test cases in pre-order.
245TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite")
246TestcaseAggregateReturnType = Tuple[int, int, int, int, int, int, timedelta]
247TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int, int, int, int, int, int, int, int, int, timedelta]
250@export
251class Base(metaclass=ExtendedType, slots=True):
252 """
253 Base-class for all test entities (test cases, test suites, ...).
255 It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity
256 hierarchy.
258 Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the
259 child. |br|
260 E.g. it's used as a test case name in the dictionary of test cases in a test suite.
262 Every test entity has fields for time tracking. If known, a start time and a test duration can be set. For more
263 details, a setup duration and teardown duration can be added. All durations are summed up in a total duration field.
265 As tests can have warnings and errors or even fail, these messages are counted and aggregated in the test entity
266 hierarchy.
268 Every test entity offers an internal dictionary for annotations. |br|
269 This feature is for example used by Ant + JUnit4's XML property fields.
270 """
272 _parent: Nullable["TestsuiteBase"]
273 _name: str
275 _startTime: Nullable[datetime]
276 _setupDuration: Nullable[timedelta]
277 _testDuration: Nullable[timedelta]
278 _teardownDuration: Nullable[timedelta]
279 _totalDuration: Nullable[timedelta]
281 _warningCount: int
282 _errorCount: int
283 _fatalCount: int
285 _expectedWarningCount: int
286 _expectedErrorCount: int
287 _expectedFatalCount: int
289 _dict: Dict[str, Any]
291 def __init__(
292 self,
293 name: str,
294 startTime: Nullable[datetime] = None,
295 setupDuration: Nullable[timedelta] = None,
296 testDuration: Nullable[timedelta] = None,
297 teardownDuration: Nullable[timedelta] = None,
298 totalDuration: Nullable[timedelta] = None,
299 warningCount: int = 0,
300 errorCount: int = 0,
301 fatalCount: int = 0,
302 expectedWarningCount: int = 0,
303 expectedErrorCount: int = 0,
304 expectedFatalCount: int = 0,
305 keyValuePairs: Nullable[Mapping[str, Any]] = None,
306 parent: Nullable["TestsuiteBase"] = None
307 ) -> None:
308 """
309 Initializes the fields of the base-class.
311 :param name: Name of the test entity.
312 :param startTime: Time when the test entity was started.
313 :param setupDuration: Duration it took to set up the entity.
314 :param testDuration: Duration of the entity's test run.
315 :param teardownDuration: Duration it took to tear down the entity.
316 :param totalDuration: Total duration of the entity's execution (setup + test + teardown).
317 :param warningCount: Count of encountered warnings.
318 :param errorCount: Count of encountered errors.
319 :param fatalCount: Count of encountered fatal errors.
320 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with.
321 :param parent: Reference to the parent test entity.
322 :raises TypeError: When parameter 'parent' is not a TestsuiteBase.
323 :raises ValueError: When parameter 'name' is None.
324 :raises TypeError: When parameter 'name' is not a string.
325 :raises ValueError: When parameter 'name' is empty.
326 :raises TypeError: When parameter 'testDuration' is not a timedelta.
327 :raises TypeError: When parameter 'setupDuration' is not a timedelta.
328 :raises TypeError: When parameter 'teardownDuration' is not a timedelta.
329 :raises TypeError: When parameter 'totalDuration' is not a timedelta.
330 :raises TypeError: When parameter 'warningCount' is not an integer.
331 :raises TypeError: When parameter 'errorCount' is not an integer.
332 :raises TypeError: When parameter 'fatalCount' is not an integer.
333 :raises TypeError: When parameter 'expectedWarningCount' is not an integer.
334 :raises TypeError: When parameter 'expectedErrorCount' is not an integer.
335 :raises TypeError: When parameter 'expectedFatalCount' is not an integer.
336 :raises TypeError: When parameter 'keyValuePairs' is not a Mapping.
337 :raises ValueError: When parameter 'totalDuration' is not consistent.
338 """
340 if parent is not None and not isinstance(parent, TestsuiteBase): 340 ↛ 341line 340 didn't jump to line 341 because the condition on line 340 was never true
341 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
342 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
343 raise ex
345 if name is None:
346 raise ValueError(f"Parameter 'name' is None.")
347 elif not isinstance(name, str): 347 ↛ 348line 347 didn't jump to line 348 because the condition on line 347 was never true
348 ex = TypeError(f"Parameter 'name' is not of type 'str'.")
349 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
350 raise ex
351 elif name.strip() == "": 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true
352 raise ValueError(f"Parameter 'name' is empty.")
354 self._parent = parent
355 self._name = name
357 if testDuration is not None and not isinstance(testDuration, timedelta): 357 ↛ 358line 357 didn't jump to line 358 because the condition on line 357 was never true
358 ex = TypeError(f"Parameter 'testDuration' is not of type 'timedelta'.")
359 ex.add_note(f"Got type '{getFullyQualifiedName(testDuration)}'.")
360 raise ex
362 if setupDuration is not None and not isinstance(setupDuration, timedelta): 362 ↛ 363line 362 didn't jump to line 363 because the condition on line 362 was never true
363 ex = TypeError(f"Parameter 'setupDuration' is not of type 'timedelta'.")
364 ex.add_note(f"Got type '{getFullyQualifiedName(setupDuration)}'.")
365 raise ex
367 if teardownDuration is not None and not isinstance(teardownDuration, timedelta): 367 ↛ 368line 367 didn't jump to line 368 because the condition on line 367 was never true
368 ex = TypeError(f"Parameter 'teardownDuration' is not of type 'timedelta'.")
369 ex.add_note(f"Got type '{getFullyQualifiedName(teardownDuration)}'.")
370 raise ex
372 if totalDuration is not None and not isinstance(totalDuration, timedelta): 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true
373 ex = TypeError(f"Parameter 'totalDuration' is not of type 'timedelta'.")
374 ex.add_note(f"Got type '{getFullyQualifiedName(totalDuration)}'.")
375 raise ex
377 if testDuration is not None:
378 if setupDuration is not None: 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true
379 if teardownDuration is not None:
380 if totalDuration is not None:
381 if totalDuration < (setupDuration + testDuration + teardownDuration):
382 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup, test and teardown durations.")
383 else: # no total
384 totalDuration = setupDuration + testDuration + teardownDuration
385 # no teardown
386 elif totalDuration is not None:
387 if totalDuration < (setupDuration + testDuration):
388 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup and test durations.")
389 # no teardown, no total
390 else:
391 totalDuration = setupDuration + testDuration
392 # no setup
393 elif teardownDuration is not None: 393 ↛ 394line 393 didn't jump to line 394 because the condition on line 393 was never true
394 if totalDuration is not None:
395 if totalDuration < (testDuration + teardownDuration):
396 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of test and teardown durations.")
397 else: # no setup, no total
398 totalDuration = testDuration + teardownDuration
399 # no setup, no teardown
400 elif totalDuration is not None:
401 if totalDuration < testDuration: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true
402 raise ValueError(f"Parameter 'totalDuration' can not be less than test durations.")
403 else: # no setup, no teardown, no total
404 totalDuration = testDuration
405 # no test
406 elif totalDuration is not None:
407 testDuration = totalDuration
408 if setupDuration is not None: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 testDuration -= setupDuration
410 if teardownDuration is not None: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 testDuration -= teardownDuration
413 self._startTime = startTime
414 self._setupDuration = setupDuration
415 self._testDuration = testDuration
416 self._teardownDuration = teardownDuration
417 self._totalDuration = totalDuration
419 if not isinstance(warningCount, int): 419 ↛ 420line 419 didn't jump to line 420 because the condition on line 419 was never true
420 ex = TypeError(f"Parameter 'warningCount' is not of type 'int'.")
421 ex.add_note(f"Got type '{getFullyQualifiedName(warningCount)}'.")
422 raise ex
424 if not isinstance(errorCount, int): 424 ↛ 425line 424 didn't jump to line 425 because the condition on line 424 was never true
425 ex = TypeError(f"Parameter 'errorCount' is not of type 'int'.")
426 ex.add_note(f"Got type '{getFullyQualifiedName(errorCount)}'.")
427 raise ex
429 if not isinstance(fatalCount, int): 429 ↛ 430line 429 didn't jump to line 430 because the condition on line 429 was never true
430 ex = TypeError(f"Parameter 'fatalCount' is not of type 'int'.")
431 ex.add_note(f"Got type '{getFullyQualifiedName(fatalCount)}'.")
432 raise ex
434 if not isinstance(expectedWarningCount, int): 434 ↛ 435line 434 didn't jump to line 435 because the condition on line 434 was never true
435 ex = TypeError(f"Parameter 'expectedWarningCount' is not of type 'int'.")
436 ex.add_note(f"Got type '{getFullyQualifiedName(expectedWarningCount)}'.")
437 raise ex
439 if not isinstance(expectedErrorCount, int): 439 ↛ 440line 439 didn't jump to line 440 because the condition on line 439 was never true
440 ex = TypeError(f"Parameter 'expectedErrorCount' is not of type 'int'.")
441 ex.add_note(f"Got type '{getFullyQualifiedName(expectedErrorCount)}'.")
442 raise ex
444 if not isinstance(expectedFatalCount, int): 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true
445 ex = TypeError(f"Parameter 'expectedFatalCount' is not of type 'int'.")
446 ex.add_note(f"Got type '{getFullyQualifiedName(expectedFatalCount)}'.")
447 raise ex
449 self._warningCount = warningCount
450 self._errorCount = errorCount
451 self._fatalCount = fatalCount
452 self._expectedWarningCount = expectedWarningCount
453 self._expectedErrorCount = expectedErrorCount
454 self._expectedFatalCount = expectedFatalCount
456 if keyValuePairs is not None and not isinstance(keyValuePairs, Mapping): 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true
457 ex = TypeError(f"Parameter 'keyValuePairs' is not a mapping.")
458 ex.add_note(f"Got type '{getFullyQualifiedName(keyValuePairs)}'.")
459 raise ex
461 self._dict = {} if keyValuePairs is None else {k: v for k, v in keyValuePairs}
463 # QUESTION: allow Parent as setter?
464 @readonly
465 def Parent(self) -> Nullable["TestsuiteBase"]:
466 """
467 Read-only property to access the reference to the parent test entity.
469 :returns: Reference to the parent entity.
470 """
471 return self._parent
473 @readonly
474 def Name(self) -> str:
475 """
476 Read-only property to access the test entity's name.
478 :returns: The test entities name.
479 """
480 return self._name
482 @readonly
483 def StartTime(self) -> Nullable[datetime]:
484 """
485 Read-only property to access the time when the test entity was started.
487 :returns: Time when the test entity was started.
488 """
489 return self._startTime
491 @readonly
492 def SetupDuration(self) -> Nullable[timedelta]:
493 """
494 Read-only property to access the duration of the test entity's setup.
496 :returns: Duration it took to set up the entity.
497 """
498 return self._setupDuration
500 @readonly
501 def TestDuration(self) -> Nullable[timedelta]:
502 """
503 Read-only property to access the duration of a test entities run.
505 This duration is excluding setup and teardown durations. In case setup and/or teardown durations are unknown or not
506 distinguishable, assign setup and teardown durations with zero.
508 :returns: Duration of the entity's test run.
509 """
510 return self._testDuration
512 @readonly
513 def TeardownDuration(self) -> Nullable[timedelta]:
514 """
515 Read-only property to access the duration of the test entity's teardown.
517 :returns: Duration it took to tear down the entity.
518 """
519 return self._teardownDuration
521 @readonly
522 def TotalDuration(self) -> Nullable[timedelta]:
523 """
524 Read-only property to access the total duration of a test entity run.
526 this duration includes setup and teardown durations.
528 :returns: Total duration of the entity's execution (setup + test + teardown)
529 """
530 return self._totalDuration
532 @readonly
533 def WarningCount(self) -> int:
534 """
535 Read-only property to access the number of encountered warnings.
537 :returns: Count of encountered warnings.
538 """
539 return self._warningCount
541 @readonly
542 def ErrorCount(self) -> int:
543 """
544 Read-only property to access the number of encountered errors.
546 :returns: Count of encountered errors.
547 """
548 return self._errorCount
550 @readonly
551 def FatalCount(self) -> int:
552 """
553 Read-only property to access the number of encountered fatal errors.
555 :returns: Count of encountered fatal errors.
556 """
557 return self._fatalCount
559 @readonly
560 def ExpectedWarningCount(self) -> int:
561 """
562 Read-only property to access the number of expected warnings.
564 :returns: Count of expected warnings.
565 """
566 return self._expectedWarningCount
568 @readonly
569 def ExpectedErrorCount(self) -> int:
570 """
571 Read-only property to access the number of expected errors.
573 :returns: Count of expected errors.
574 """
575 return self._expectedErrorCount
577 @readonly
578 def ExpectedFatalCount(self) -> int:
579 """
580 Read-only property to access the number of expected fatal errors.
582 :returns: Count of expected fatal errors.
583 """
584 return self._expectedFatalCount
586 def __len__(self) -> int:
587 """
588 Returns the number of annotated key-value pairs.
590 :returns: Number of annotated key-value pairs.
591 """
592 return len(self._dict)
594 def __getitem__(self, key: str) -> Any:
595 """
596 Access a key-value pair by key.
598 :param key: Name if the key-value pair.
599 :returns: Value of the accessed key.
600 """
601 return self._dict[key]
603 def __setitem__(self, key: str, value: Any) -> None:
604 """
605 Set the value of a key-value pair by key.
607 If the pair doesn't exist yet, it's created.
609 :param key: Key of the key-value pair.
610 :param value: Value of the key-value pair.
611 """
612 self._dict[key] = value
614 def __delitem__(self, key: str) -> None:
615 """
616 Delete a key-value pair by key.
618 :param key: Name if the key-value pair.
619 """
620 del self._dict[key]
622 def __contains__(self, key: str) -> bool:
623 """
624 Returns True, if a key-value pairs was annotated by this key.
626 :param key: Name of the key-value pair.
627 :returns: True, if the pair was annotated.
628 """
629 return key in self._dict
631 def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
632 """
633 Iterate all annotated key-value pairs.
635 :returns: A generator of key-value pair tuples (key, value).
636 """
637 yield from self._dict.items()
639 @abstractmethod
640 def Aggregate(self, strict: bool = True) -> None:
641 """
642 Aggregate all test entities in the hierarchy.
643 """
645 @abstractmethod
646 def __str__(self) -> str:
647 """
648 Formats the test entity as human-readable incl. some statistics.
649 """
652@export
653class Testcase(Base):
654 """
655 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run.
657 Test cases are grouped by test suites in the test entity hierarchy. The root of the hierarchy is a test summary.
659 Every test case has an overall status like unknown, skipped, failed or passed.
661 In addition to all features from its base-class, test cases provide additional statistics for passed and failed
662 assertions (checks) as well as a sum thereof.
663 """
665 _status: TestcaseStatus
666 _assertionCount: Nullable[int]
667 _failedAssertionCount: Nullable[int]
668 _passedAssertionCount: Nullable[int]
670 def __init__(
671 self,
672 name: str,
673 startTime: Nullable[datetime] = None,
674 setupDuration: Nullable[timedelta] = None,
675 testDuration: Nullable[timedelta] = None,
676 teardownDuration: Nullable[timedelta] = None,
677 totalDuration: Nullable[timedelta] = None,
678 status: TestcaseStatus = TestcaseStatus.Unknown,
679 assertionCount: Nullable[int] = None,
680 failedAssertionCount: Nullable[int] = None,
681 passedAssertionCount: Nullable[int] = None,
682 warningCount: int = 0,
683 errorCount: int = 0,
684 fatalCount: int = 0,
685 expectedWarningCount: int = 0,
686 expectedErrorCount: int = 0,
687 expectedFatalCount: int = 0,
688 keyValuePairs: Nullable[Mapping[str, Any]] = None,
689 parent: Nullable["Testsuite"] = None
690 ) -> None:
691 """
692 Initializes the fields of a test case.
694 :param name: Name of the test entity.
695 :param startTime: Time when the test entity was started.
696 :param setupDuration: Duration it took to set up the entity.
697 :param testDuration: Duration of the entity's test run.
698 :param teardownDuration: Duration it took to tear down the entity.
699 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
700 :param status: Status of the test case.
701 :param assertionCount: Number of assertions within the test.
702 :param failedAssertionCount: Number of failed assertions within the test.
703 :param passedAssertionCount: Number of passed assertions within the test.
704 :param warningCount: Count of encountered warnings.
705 :param errorCount: Count of encountered errors.
706 :param fatalCount: Count of encountered fatal errors.
707 :param keyValuePairs: Mapping of key-value pairs to initialize the test case.
708 :param parent: Reference to the parent test suite.
709 :raises TypeError: If parameter 'parent' is not a Testsuite.
710 :raises ValueError: If parameter 'assertionCount' is not consistent.
711 """
713 if parent is not None:
714 if not isinstance(parent, Testsuite):
715 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.")
716 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
717 raise ex
719 parent._testcases[name] = self
721 super().__init__(
722 name,
723 startTime,
724 setupDuration, testDuration, teardownDuration, totalDuration,
725 warningCount, errorCount, fatalCount,
726 expectedWarningCount, expectedErrorCount, expectedFatalCount,
727 keyValuePairs,
728 parent=parent
729 )
731 if not isinstance(status, TestcaseStatus): 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true
732 ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.")
733 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.")
734 raise ex
736 self._status = status
738 if assertionCount is not None and not isinstance(assertionCount, int): 738 ↛ 739line 738 didn't jump to line 739 because the condition on line 738 was never true
739 ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.")
740 ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.")
741 raise ex
743 if failedAssertionCount is not None and not isinstance(failedAssertionCount, int): 743 ↛ 744line 743 didn't jump to line 744 because the condition on line 743 was never true
744 ex = TypeError(f"Parameter 'failedAssertionCount' is not of type 'int'.")
745 ex.add_note(f"Got type '{getFullyQualifiedName(failedAssertionCount)}'.")
746 raise ex
748 if passedAssertionCount is not None and not isinstance(passedAssertionCount, int): 748 ↛ 749line 748 didn't jump to line 749 because the condition on line 748 was never true
749 ex = TypeError(f"Parameter 'passedAssertionCount' is not of type 'int'.")
750 ex.add_note(f"Got type '{getFullyQualifiedName(passedAssertionCount)}'.")
751 raise ex
753 self._assertionCount = assertionCount
754 if assertionCount is not None:
755 if failedAssertionCount is not None:
756 self._failedAssertionCount = failedAssertionCount
758 if passedAssertionCount is not None:
759 if passedAssertionCount + failedAssertionCount != assertionCount:
760 raise ValueError(f"passed assertion count ({passedAssertionCount}) + failed assertion count ({failedAssertionCount} != assertion count ({assertionCount}")
762 self._passedAssertionCount = passedAssertionCount
763 else:
764 self._passedAssertionCount = assertionCount - failedAssertionCount
765 elif passedAssertionCount is not None:
766 self._passedAssertionCount = passedAssertionCount
767 self._failedAssertionCount = assertionCount - passedAssertionCount
768 else:
769 raise ValueError(f"Neither passed assertion count nor failed assertion count are provided.")
770 elif failedAssertionCount is not None:
771 self._failedAssertionCount = failedAssertionCount
773 if passedAssertionCount is not None:
774 self._passedAssertionCount = passedAssertionCount
775 self._assertionCount = passedAssertionCount + failedAssertionCount
776 else:
777 raise ValueError(f"Passed assertion count is mandatory, if failed assertion count is provided instead of assertion count.")
778 elif passedAssertionCount is not None:
779 raise ValueError(f"Assertion count or failed assertion count is mandatory, if passed assertion count is provided.")
780 else:
781 self._passedAssertionCount = None
782 self._failedAssertionCount = None
784 @readonly
785 def Status(self) -> TestcaseStatus:
786 """
787 Read-only property to access the status of the test case.
789 :returns: The test case's status.
790 """
791 return self._status
793 @readonly
794 def AssertionCount(self) -> int:
795 """
796 Read-only property to return the number of assertions (checks) in a test case.
798 :returns: Number of assertions.
799 """
800 if self._assertionCount is None:
801 return 0
802 return self._assertionCount
804 @readonly
805 def FailedAssertionCount(self) -> int:
806 """
807 Read-only property to access the number of failed assertions (failed checks) in a test case.
809 :returns: Number of assertions.
810 """
811 return self._failedAssertionCount
813 @readonly
814 def PassedAssertionCount(self) -> int:
815 """
816 Read-only property to access the number of passed assertions (successful checks) in a test case.
818 :returns: Number of passed assertions.
819 """
820 return self._passedAssertionCount
822 def Copy(self) -> "Testcase":
823 return self.__class__(
824 self._name,
825 self._startTime,
826 self._setupDuration,
827 self._testDuration,
828 self._teardownDuration,
829 self._totalDuration,
830 self._status,
831 self._warningCount,
832 self._errorCount,
833 self._fatalCount,
834 self._expectedWarningCount,
835 self._expectedErrorCount,
836 self._expectedFatalCount,
837 )
838 # TODO: copy key-value-pairs?
840 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType:
841 if self._status is TestcaseStatus.Unknown:
842 if self._assertionCount is None: 842 ↛ 843line 842 didn't jump to line 843 because the condition on line 842 was never true
843 self._status = TestcaseStatus.Passed
844 elif self._assertionCount == 0: 844 ↛ 845line 844 didn't jump to line 845 because the condition on line 844 was never true
845 self._status = TestcaseStatus.Weak
846 elif self._failedAssertionCount == 0:
847 self._status = TestcaseStatus.Passed
848 else:
849 self._status = TestcaseStatus.Failed
851 if self._warningCount - self._expectedWarningCount > 0: 851 ↛ 852line 851 didn't jump to line 852 because the condition on line 851 was never true
852 self._status |= TestcaseStatus.Warned
854 if self._errorCount - self._expectedErrorCount > 0: 854 ↛ 855line 854 didn't jump to line 855 because the condition on line 854 was never true
855 self._status |= TestcaseStatus.Errored
857 if self._fatalCount - self._expectedFatalCount > 0: 857 ↛ 858line 857 didn't jump to line 858 because the condition on line 857 was never true
858 self._status |= TestcaseStatus.Aborted
860 if strict:
861 self._status = self._status & ~TestcaseStatus.Passed | TestcaseStatus.Failed
863 # TODO: check for setup errors
864 # TODO: check for teardown errors
866 totalDuration = timedelta() if self._totalDuration is None else self._totalDuration
868 return self._warningCount, self._errorCount, self._fatalCount, self._expectedWarningCount, self._expectedErrorCount, self._expectedFatalCount, totalDuration
870 def __str__(self) -> str:
871 """
872 Formats the test case as human-readable incl. statistics.
874 :pycode:`f"<Testcase {}: {} - assert/pass/fail:{}/{}/{} - warn/error/fatal:{}/{}/{} - setup/test/teardown:{}/{}/{}>"`
876 :returns: Human-readable summary of a test case object.
877 """
878 return (
879 f"<Testcase {self._name}: {self._status.name} -"
880 f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
881 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount} -"
882 f" setup/test/teardown:{self._setupDuration:.3f}/{self._testDuration:.3f}/{self._teardownDuration:.3f}>"
883 )
886@export
887class TestsuiteBase(Base, Generic[TestsuiteType]):
888 """
889 Base-class for all test suites and for test summaries.
891 A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root
892 element in that hierarchy. While a test suite groups other test suites and test cases, a test summary can only group
893 test suites. Thus, a test summary contains no test cases.
894 """
896 _kind: TestsuiteKind
897 _status: TestsuiteStatus
898 _testsuites: Dict[str, TestsuiteType]
900 _tests: int
901 _inconsistent: int
902 _excluded: int
903 _skipped: int
904 _errored: int
905 _weak: int
906 _failed: int
907 _passed: int
909 def __init__(
910 self,
911 name: str,
912 kind: TestsuiteKind = TestsuiteKind.Logical,
913 startTime: Nullable[datetime] = None,
914 setupDuration: Nullable[timedelta] = None,
915 testDuration: Nullable[timedelta] = None,
916 teardownDuration: Nullable[timedelta] = None,
917 totalDuration: Nullable[timedelta] = None,
918 status: TestsuiteStatus = TestsuiteStatus.Unknown,
919 warningCount: int = 0,
920 errorCount: int = 0,
921 fatalCount: int = 0,
922 testsuites: Nullable[Iterable[TestsuiteType]] = None,
923 keyValuePairs: Nullable[Mapping[str, Any]] = None,
924 parent: Nullable["Testsuite"] = None
925 ) -> None:
926 """
927 Initializes the based-class fields of a test suite or test summary.
929 :param name: Name of the test entity.
930 :param kind: Kind of the test entity.
931 :param startTime: Time when the test entity was started.
932 :param setupDuration: Duration it took to set up the entity.
933 :param testDuration: Duration of all tests listed in the test entity.
934 :param teardownDuration: Duration it took to tear down the entity.
935 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
936 :param status: Overall status of the test entity.
937 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
938 :param errorCount: Count of encountered errors incl. errors from sub-elements.
939 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
940 :param testsuites: List of test suites to initialize the test entity with.
941 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with.
942 :param parent: Reference to the parent test entity.
943 :raises TypeError: If parameter 'parent' is not a TestsuiteBase.
944 :raises TypeError: If parameter 'testsuites' is not iterable.
945 :raises TypeError: If element in parameter 'testsuites' is not a Testsuite.
946 :raises AlreadyInHierarchyException: If a test suite in parameter 'testsuites' is already part of a test entity hierarchy.
947 :raises DuplicateTestsuiteException: If a test suite in parameter 'testsuites' is already listed (by name) in the list of test suites.
948 """
949 if parent is not None:
950 if not isinstance(parent, TestsuiteBase): 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true
951 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
952 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
953 raise ex
955 parent._testsuites[name] = self
957 super().__init__(
958 name,
959 startTime,
960 setupDuration,
961 testDuration,
962 teardownDuration,
963 totalDuration,
964 warningCount,
965 errorCount,
966 fatalCount,
967 0, 0, 0,
968 keyValuePairs,
969 parent=parent
970 )
972 self._kind = kind
973 self._status = status
975 self._testsuites = {}
976 if testsuites is not None:
977 if not isinstance(testsuites, Iterable): 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true
978 ex = TypeError(f"Parameter 'testsuites' is not iterable.")
979 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.")
980 raise ex
982 for testsuite in testsuites:
983 if not isinstance(testsuite, Testsuite): 983 ↛ 984line 983 didn't jump to line 984 because the condition on line 983 was never true
984 ex = TypeError(f"Element of parameter 'testsuites' is not of type 'Testsuite'.")
985 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.")
986 raise ex
988 if testsuite._parent is not None: 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true
989 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
991 if testsuite._name in self._testsuites:
992 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
994 testsuite._parent = self
995 self._testsuites[testsuite._name] = testsuite
997 self._status = TestsuiteStatus.Unknown
998 self._tests = 0
999 self._inconsistent = 0
1000 self._excluded = 0
1001 self._skipped = 0
1002 self._errored = 0
1003 self._weak = 0
1004 self._failed = 0
1005 self._passed = 0
1007 @readonly
1008 def Kind(self) -> TestsuiteKind:
1009 """
1010 Read-only property to access the kind of the test suite.
1012 Test suites are used to group test cases. This grouping can be due to language/framework specifics like tests
1013 grouped by a module file or namespace. Others might be just logically grouped without any relation to a programming
1014 language construct.
1016 Test summaries always return kind ``Root``.
1018 :returns: Kind of the test suite.
1019 """
1020 return self._kind
1022 @readonly
1023 def Status(self) -> TestsuiteStatus:
1024 """
1025 Read-only property to access the aggregated overall status of the test suite.
1027 :returns: Overall status of the test suite.
1028 """
1029 return self._status
1031 @readonly
1032 def Testsuites(self) -> Dict[str, TestsuiteType]:
1033 """
1034 Read-only property to access a reference to the internal dictionary of test suites.
1036 :returns: Reference to the dictionary of test suite.
1037 """
1038 return self._testsuites
1040 @readonly
1041 def TestsuiteCount(self) -> int:
1042 """
1043 Read-only property to return the number of all test suites in the test suite hierarchy.
1045 :returns: Number of test suites.
1046 """
1047 return 1 + sum(testsuite.TestsuiteCount for testsuite in self._testsuites.values())
1049 @readonly
1050 def TestcaseCount(self) -> int:
1051 """
1052 Read-only property to return the number of all test cases in the test entity hierarchy.
1054 :returns: Number of test cases.
1055 """
1056 return sum(testsuite.TestcaseCount for testsuite in self._testsuites.values())
1058 @readonly
1059 def AssertionCount(self) -> int:
1060 """
1061 Read-only property to return the number of all assertions in all test cases in the test entity hierarchy.
1063 :returns: Number of assertions in all test cases.
1064 """
1065 return sum(ts.AssertionCount for ts in self._testsuites.values())
1067 @readonly
1068 def FailedAssertionCount(self) -> int:
1069 """
1070 Read-only property to access the number of all failed assertions in all test cases in the test entity hierarchy.
1072 :returns: Number of failed assertions in all test cases.
1073 """
1074 raise NotImplementedError()
1075 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount)
1077 @readonly
1078 def PassedAssertionCount(self) -> int:
1079 """
1080 Read-only property to access the number of all passed assertions in all test cases in the test entity hierarchy.
1082 :returns: Number of passed assertions in all test cases.
1083 """
1084 raise NotImplementedError()
1085 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount)
1087 @readonly
1088 def Tests(self) -> int:
1089 """
1090 Read-only property to access the number of tests in this entity.
1092 :returns: Number of tests.
1093 """
1094 return self._tests
1096 @readonly
1097 def Inconsistent(self) -> int:
1098 """
1099 Read-only property to access the number of inconsistent tests in the test suite hierarchy.
1101 :returns: Number of inconsistent tests.
1102 """
1103 return self._inconsistent
1105 @readonly
1106 def Excluded(self) -> int:
1107 """
1108 Read-only property to access the number of excluded tests in the test suite hierarchy.
1110 :returns: Number of excluded tests.
1111 """
1112 return self._excluded
1114 @readonly
1115 def Skipped(self) -> int:
1116 """
1117 Read-only property to access the number of skipped tests in the test suite hierarchy.
1119 :returns: Number of skipped tests.
1120 """
1121 return self._skipped
1123 @readonly
1124 def Errored(self) -> int:
1125 """
1126 Read-only property to access the number of tests with errors in the test suite hierarchy.
1128 :returns: Number of errored tests.
1129 """
1130 return self._errored
1132 @readonly
1133 def Weak(self) -> int:
1134 """
1135 Read-only property to access the number of weak tests in the test suite hierarchy.
1137 :returns: Number of weak tests.
1138 """
1139 return self._weak
1141 @readonly
1142 def Failed(self) -> int:
1143 """
1144 Read-only property to access the number of failed tests in the test suite hierarchy.
1146 :returns: Number of failed tests.
1147 """
1148 return self._failed
1150 @readonly
1151 def Passed(self) -> int:
1152 """
1153 Read-only property to access the number of passed tests in the test suite hierarchy.
1155 :returns: Number of passed tests.
1156 """
1157 return self._passed
1159 @readonly
1160 def WarningCount(self) -> int:
1161 """
1162 Read-only property to access the number of warnings in this entity.
1164 :returns: Number of warnings.
1165 """
1166 raise NotImplementedError()
1167 # return self._warningCount
1169 @readonly
1170 def ErrorCount(self) -> int:
1171 """
1172 Read-only property to access the number of errors in this entity.
1174 :returns: Number of errors.
1175 """
1176 raise NotImplementedError()
1177 # return self._errorCount
1179 @readonly
1180 def FatalCount(self) -> int:
1181 """
1182 Read-only property to access the number of fatal errors in this entity.
1184 :returns: Number of fatal errors.
1185 """
1186 raise NotImplementedError()
1187 # return self._fatalCount
1189 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1190 tests = 0
1191 inconsistent = 0
1192 excluded = 0
1193 skipped = 0
1194 errored = 0
1195 weak = 0
1196 failed = 0
1197 passed = 0
1199 warningCount = 0
1200 errorCount = 0
1201 fatalCount = 0
1203 expectedWarningCount = 0
1204 expectedErrorCount = 0
1205 expectedFatalCount = 0
1207 totalDuration = timedelta()
1209 for testsuite in self._testsuites.values():
1210 t, i, ex, s, e, w, f, p, wc, ec, fc, ewc, eec, efc, td = testsuite.Aggregate(strict)
1211 tests += t
1212 inconsistent += i
1213 excluded += ex
1214 skipped += s
1215 errored += e
1216 weak += w
1217 failed += f
1218 passed += p
1220 warningCount += wc
1221 errorCount += ec
1222 fatalCount += fc
1224 expectedWarningCount += ewc
1225 expectedErrorCount += eec
1226 expectedFatalCount += efc
1228 totalDuration += td
1230 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration
1232 def AddTestsuite(self, testsuite: TestsuiteType) -> None:
1233 """
1234 Add a test suite to the list of test suites.
1236 :param testsuite: The test suite to add.
1237 :raises ValueError: If parameter 'testsuite' is None.
1238 :raises TypeError: If parameter 'testsuite' is not a Testsuite.
1239 :raises AlreadyInHierarchyException: If parameter 'testsuite' is already part of a test entity hierarchy.
1240 :raises DuplicateTestcaseException: If parameter 'testsuite' is already listed (by name) in the list of test suites.
1241 """
1242 if testsuite is None: 1242 ↛ 1243line 1242 didn't jump to line 1243 because the condition on line 1242 was never true
1243 raise ValueError("Parameter 'testsuite' is None.")
1244 elif not isinstance(testsuite, Testsuite): 1244 ↛ 1245line 1244 didn't jump to line 1245 because the condition on line 1244 was never true
1245 ex = TypeError(f"Parameter 'testsuite' is not of type 'Testsuite'.")
1246 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.")
1247 raise ex
1249 if testsuite._parent is not None: 1249 ↛ 1250line 1249 didn't jump to line 1250 because the condition on line 1249 was never true
1250 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1252 if testsuite._name in self._testsuites:
1253 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1255 testsuite._parent = self
1256 self._testsuites[testsuite._name] = testsuite
1258 def AddTestsuites(self, testsuites: Iterable[TestsuiteType]) -> None:
1259 """
1260 Add a list of test suites to the list of test suites.
1262 :param testsuites: List of test suites to add.
1263 :raises ValueError: If parameter 'testsuites' is None.
1264 :raises TypeError: If parameter 'testsuites' is not iterable.
1265 """
1266 if testsuites is None: 1266 ↛ 1267line 1266 didn't jump to line 1267 because the condition on line 1266 was never true
1267 raise ValueError("Parameter 'testsuites' is None.")
1268 elif not isinstance(testsuites, Iterable): 1268 ↛ 1269line 1268 didn't jump to line 1269 because the condition on line 1268 was never true
1269 ex = TypeError(f"Parameter 'testsuites' is not iterable.")
1270 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.")
1271 raise ex
1273 for testsuite in testsuites:
1274 self.AddTestsuite(testsuite)
1276 @abstractmethod
1277 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1278 pass
1280 def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
1281 return self.Iterate(scheme)
1283 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
1284 return self.Iterate(scheme)
1286 def ToTree(self) -> Node:
1287 rootNode = Node(value=self._name)
1289 def convertTestcase(testcase: Testcase, parentNode: Node) -> None:
1290 _ = Node(value=testcase._name, parent=parentNode)
1292 def convertTestsuite(testsuite: Testsuite, parentNode: Node) -> None:
1293 testsuiteNode = Node(value=testsuite._name, parent=parentNode)
1295 for ts in testsuite._testsuites.values():
1296 convertTestsuite(ts, testsuiteNode)
1298 for tc in testsuite._testcases.values():
1299 convertTestcase(tc, testsuiteNode)
1301 for testsuite in self._testsuites.values():
1302 convertTestsuite(testsuite, rootNode)
1304 return rootNode
1307@export
1308class Testsuite(TestsuiteBase[TestsuiteType]):
1309 """
1310 A testsuite is a mid-level element in the test entity hierarchy representing a group of tests.
1312 Test suites contain test cases and optionally other test suites. Test suites can be grouped by test suites to form a
1313 hierarchy of test entities. The root of the hierarchy is a test summary.
1314 """
1316 _testcases: Dict[str, "Testcase"]
1317 _hostname: Nullable[str]
1319 def __init__(
1320 self,
1321 name: str,
1322 kind: TestsuiteKind = TestsuiteKind.Logical,
1323 hostname: Nullable[str] = None,
1324 startTime: Nullable[datetime] = None,
1325 setupDuration: Nullable[timedelta] = None,
1326 testDuration: Nullable[timedelta] = None,
1327 teardownDuration: Nullable[timedelta] = None,
1328 totalDuration: Nullable[timedelta] = None,
1329 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1330 warningCount: int = 0,
1331 errorCount: int = 0,
1332 fatalCount: int = 0,
1333 testsuites: Nullable[Iterable[TestsuiteType]] = None,
1334 testcases: Nullable[Iterable["Testcase"]] = None,
1335 keyValuePairs: Nullable[Mapping[str, Any]] = None,
1336 parent: Nullable[TestsuiteType] = None
1337 ) -> None:
1338 """
1339 Initializes the fields of a test suite.
1341 :param name: Name of the test suite.
1342 :param kind: Kind of the test suite.
1343 :param hostname: Name of the host the test suite was executed on, or ``None`` if it wasn't recorded.
1344 :param startTime: Time when the test suite was started.
1345 :param setupDuration: Duration it took to set up the test suite.
1346 :param testDuration: Duration of all tests listed in the test suite.
1347 :param teardownDuration: Duration it took to tear down the test suite.
1348 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
1349 :param status: Overall status of the test suite.
1350 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
1351 :param errorCount: Count of encountered errors incl. errors from sub-elements.
1352 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
1353 :param testsuites: List of test suites to initialize the test suite with.
1354 :param testcases: List of test cases to initialize the test suite with.
1355 :param keyValuePairs: Mapping of key-value pairs to initialize the test suite with.
1356 :param parent: Reference to the parent test entity.
1357 :raises TypeError: If parameter 'testcases' is not iterable.
1358 :raises TypeError: If element in parameter 'testcases' is not a Testcase.
1359 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy.
1360 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases.
1361 """
1362 super().__init__(
1363 name,
1364 kind,
1365 startTime,
1366 setupDuration,
1367 testDuration,
1368 teardownDuration,
1369 totalDuration,
1370 status,
1371 warningCount,
1372 errorCount,
1373 fatalCount,
1374 testsuites,
1375 keyValuePairs,
1376 parent=parent
1377 )
1379 # self._testDuration = testDuration
1380 self._hostname = hostname
1382 self._testcases = {}
1383 if testcases is not None:
1384 if not isinstance(testcases, Iterable): 1384 ↛ 1385line 1384 didn't jump to line 1385 because the condition on line 1384 was never true
1385 ex = TypeError(f"Parameter 'testcases' is not iterable.")
1386 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.")
1387 raise ex
1389 for testcase in testcases:
1390 if not isinstance(testcase, Testcase): 1390 ↛ 1391line 1390 didn't jump to line 1391 because the condition on line 1390 was never true
1391 ex = TypeError(f"Element of parameter 'testcases' is not of type 'Testcase'.")
1392 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.")
1393 raise ex
1395 if testcase._parent is not None: 1395 ↛ 1396line 1395 didn't jump to line 1396 because the condition on line 1395 was never true
1396 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
1398 if testcase._name in self._testcases:
1399 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1401 testcase._parent = self
1402 self._testcases[testcase._name] = testcase
1404 @readonly
1405 def Testcases(self) -> Dict[str, "Testcase"]:
1406 """
1407 Read-only property to access a reference to the internal dictionary of test cases.
1409 :returns: Reference to the dictionary of test cases.
1410 """
1411 return self._testcases
1413 @readonly
1414 def TestcaseCount(self) -> int:
1415 """
1416 Read-only property to return the number of all test cases in the test entity hierarchy.
1418 :returns: Number of test cases.
1419 """
1420 return super().TestcaseCount + len(self._testcases)
1422 @readonly
1423 def AssertionCount(self) -> int:
1424 """
1425 Read-only property to return the number of assertions in this testsuite and its testcases.
1427 :returns: Sum of the inherited assertion count and the testcases' assertion counts.
1428 """
1429 return super().AssertionCount + sum(tc.AssertionCount for tc in self._testcases.values())
1431 @readonly
1432 def Hostname(self) -> Nullable[str]:
1433 """
1434 Read-only property to access the name of the host this test suite was executed on (:attr:`_hostname`).
1436 :returns: The hostname, or ``None`` if it wasn't recorded.
1437 """
1438 return self._hostname
1440 def Copy(self) -> "Testsuite":
1441 return self.__class__(
1442 self._name,
1443 self._kind,
1444 self._hostname,
1445 self._startTime,
1446 self._setupDuration,
1447 self._testDuration,
1448 self._teardownDuration,
1449 self._totalDuration,
1450 self._status,
1451 self._warningCount,
1452 self._errorCount,
1453 self._fatalCount
1454 )
1456 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1457 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration = super().Aggregate()
1459 for testcase in self._testcases.values():
1460 wc, ec, fc, ewc, eec, efc, td = testcase.Aggregate(strict)
1462 tests += 1
1464 warningCount += wc
1465 errorCount += ec
1466 fatalCount += fc
1468 expectedWarningCount += ewc
1469 expectedErrorCount += eec
1470 expectedFatalCount += efc
1472 totalDuration += td
1474 status = testcase._status
1475 if status is TestcaseStatus.Unknown: 1475 ↛ 1476line 1475 didn't jump to line 1476 because the condition on line 1475 was never true
1476 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.")
1477 elif TestcaseStatus.Inconsistent in status: 1477 ↛ 1478line 1477 didn't jump to line 1478 because the condition on line 1477 was never true
1478 inconsistent += 1
1479 elif status is TestcaseStatus.Excluded: 1479 ↛ 1480line 1479 didn't jump to line 1480 because the condition on line 1479 was never true
1480 excluded += 1
1481 elif status is TestcaseStatus.Skipped:
1482 skipped += 1
1483 elif status is TestcaseStatus.Errored: 1483 ↛ 1484line 1483 didn't jump to line 1484 because the condition on line 1483 was never true
1484 errored += 1
1485 elif status is TestcaseStatus.Weak: 1485 ↛ 1486line 1485 didn't jump to line 1486 because the condition on line 1485 was never true
1486 weak += 1
1487 elif status is TestcaseStatus.Passed:
1488 passed += 1
1489 elif status is TestcaseStatus.Failed: 1489 ↛ 1491line 1489 didn't jump to line 1491 because the condition on line 1489 was always true
1490 failed += 1
1491 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown:
1492 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.")
1493 else:
1494 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.")
1496 self._tests = tests
1497 self._inconsistent = inconsistent
1498 self._excluded = excluded
1499 self._skipped = skipped
1500 self._errored = errored
1501 self._weak = weak
1502 self._failed = failed
1503 self._passed = passed
1505 self._warningCount = warningCount
1506 self._errorCount = errorCount
1507 self._fatalCount = fatalCount
1509 self._expectedWarningCount = expectedWarningCount
1510 self._expectedErrorCount = expectedErrorCount
1511 self._expectedFatalCount = expectedFatalCount
1513 if self._totalDuration is None:
1514 self._totalDuration = totalDuration
1516 if errored > 0: 1516 ↛ 1517line 1516 didn't jump to line 1517 because the condition on line 1516 was never true
1517 self._status = TestsuiteStatus.Errored
1518 elif failed > 0:
1519 self._status = TestsuiteStatus.Failed
1520 elif tests == 0: 1520 ↛ 1521line 1520 didn't jump to line 1521 because the condition on line 1520 was never true
1521 self._status = TestsuiteStatus.Empty
1522 elif tests - skipped == passed: 1522 ↛ 1524line 1522 didn't jump to line 1524 because the condition on line 1522 was always true
1523 self._status = TestsuiteStatus.Passed
1524 elif tests == skipped:
1525 self._status = TestsuiteStatus.Skipped
1526 else:
1527 self._status = TestsuiteStatus.Unknown
1529 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration
1531 def AddTestcase(self, testcase: "Testcase") -> None:
1532 """
1533 Add a test case to the list of test cases.
1535 :param testcase: The test case to add.
1536 :raises ValueError: If parameter 'testcase' is None.
1537 :raises TypeError: If parameter 'testcase' is not a Testcase.
1538 :raises AlreadyInHierarchyException: If parameter 'testcase' is already part of a test entity hierarchy.
1539 :raises DuplicateTestcaseException: If parameter 'testcase' is already listed (by name) in the list of test cases.
1540 """
1541 if testcase is None: 1541 ↛ 1542line 1541 didn't jump to line 1542 because the condition on line 1541 was never true
1542 raise ValueError("Parameter 'testcase' is None.")
1543 elif not isinstance(testcase, Testcase): 1543 ↛ 1544line 1543 didn't jump to line 1544 because the condition on line 1543 was never true
1544 ex = TypeError(f"Parameter 'testcase' is not of type 'Testcase'.")
1545 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.")
1546 raise ex
1548 if testcase._parent is not None: 1548 ↛ 1549line 1548 didn't jump to line 1549 because the condition on line 1548 was never true
1549 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
1551 if testcase._name in self._testcases:
1552 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1554 testcase._parent = self
1555 self._testcases[testcase._name] = testcase
1557 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None:
1558 """
1559 Add a list of test cases to the list of test cases.
1561 :param testcases: List of test cases to add.
1562 :raises ValueError: If parameter 'testcases' is None.
1563 :raises TypeError: If parameter 'testcases' is not iterable.
1564 """
1565 if testcases is None: 1565 ↛ 1566line 1565 didn't jump to line 1566 because the condition on line 1565 was never true
1566 raise ValueError("Parameter 'testcases' is None.")
1567 elif not isinstance(testcases, Iterable): 1567 ↛ 1568line 1567 didn't jump to line 1568 because the condition on line 1567 was never true
1568 ex = TypeError(f"Parameter 'testcases' is not iterable.")
1569 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.")
1570 raise ex
1572 for testcase in testcases:
1573 self.AddTestcase(testcase)
1575 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1576 if IterationScheme.PreOrder in scheme:
1577 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1578 yield self
1580 if IterationScheme.IncludeTestcases in scheme:
1581 for testcase in self._testcases.values():
1582 yield testcase
1584 for testsuite in self._testsuites.values():
1585 yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf)
1587 if IterationScheme.PostOrder in scheme:
1588 if IterationScheme.IncludeTestcases in scheme:
1589 for testcase in self._testcases.values():
1590 yield testcase
1592 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1593 yield self
1595 def __str__(self) -> str:
1596 return (
1597 f"<Testsuite {self._name}: {self._status.name} -"
1598 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
1599 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>"
1600 )
1603@export
1604class TestsuiteSummary(TestsuiteBase[TestsuiteType]):
1605 """
1606 A testsuite summary is the root element in the test entity hierarchy representing a summary of all test suites and cases.
1608 The testsuite summary contains test suites, which in turn can contain test suites and test cases.
1609 """
1611 def __init__(
1612 self,
1613 name: str,
1614 startTime: Nullable[datetime] = None,
1615 setupDuration: Nullable[timedelta] = None,
1616 testDuration: Nullable[timedelta] = None,
1617 teardownDuration: Nullable[timedelta] = None,
1618 totalDuration: Nullable[timedelta] = None,
1619 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1620 warningCount: int = 0,
1621 errorCount: int = 0,
1622 fatalCount: int = 0,
1623 testsuites: Nullable[Iterable[TestsuiteType]] = None,
1624 keyValuePairs: Nullable[Mapping[str, Any]] = None,
1625 parent: Nullable[TestsuiteType] = None
1626 ) -> None:
1627 """
1628 Initializes the fields of a test summary.
1630 :param name: Name of the test summary.
1631 :param startTime: Time when the test summary was started.
1632 :param setupDuration: Duration it took to set up the test summary.
1633 :param testDuration: Duration of all tests listed in the test summary.
1634 :param teardownDuration: Duration it took to tear down the test summary.
1635 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
1636 :param status: Overall status of the test summary.
1637 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
1638 :param errorCount: Count of encountered errors incl. errors from sub-elements.
1639 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
1640 :param testsuites: List of test suites to initialize the test summary with.
1641 :param keyValuePairs: Mapping of key-value pairs to initialize the test summary with.
1642 :param parent: Reference to the parent test summary.
1643 """
1644 super().__init__(
1645 name,
1646 TestsuiteKind.Root,
1647 startTime, setupDuration, testDuration, teardownDuration, totalDuration,
1648 status,
1649 warningCount, errorCount, fatalCount,
1650 testsuites,
1651 keyValuePairs,
1652 parent=parent
1653 )
1655 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1656 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration = super().Aggregate(strict)
1658 self._tests = tests
1659 self._inconsistent = inconsistent
1660 self._excluded = excluded
1661 self._skipped = skipped
1662 self._errored = errored
1663 self._weak = weak
1664 self._failed = failed
1665 self._passed = passed
1667 self._warningCount = warningCount
1668 self._errorCount = errorCount
1669 self._fatalCount = fatalCount
1671 self._expectedWarningCount = expectedWarningCount
1672 self._expectedErrorCount = expectedErrorCount
1673 self._expectedFatalCount = expectedFatalCount
1675 if self._totalDuration is None:
1676 self._totalDuration = totalDuration
1678 if errored > 0: 1678 ↛ 1679line 1678 didn't jump to line 1679 because the condition on line 1678 was never true
1679 self._status = TestsuiteStatus.Errored
1680 elif failed > 0:
1681 self._status = TestsuiteStatus.Failed
1682 elif tests == 0: 1682 ↛ 1683line 1682 didn't jump to line 1683 because the condition on line 1682 was never true
1683 self._status = TestsuiteStatus.Empty
1684 elif tests - skipped == passed: 1684 ↛ 1686line 1684 didn't jump to line 1686 because the condition on line 1684 was always true
1685 self._status = TestsuiteStatus.Passed
1686 elif tests == skipped:
1687 self._status = TestsuiteStatus.Skipped
1688 elif tests == excluded:
1689 self._status = TestsuiteStatus.Excluded
1690 else:
1691 self._status = TestsuiteStatus.Unknown
1693 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration
1695 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1696 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1697 yield self
1699 for testsuite in self._testsuites.values():
1700 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1702 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme: 1702 ↛ 1703line 1702 didn't jump to line 1703 because the condition on line 1702 was never true
1703 yield self
1705 def __str__(self) -> str:
1706 return (
1707 f"<TestsuiteSummary {self._name}: {self._status.name} -"
1708 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
1709 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>"
1710 )
1713@export
1714class Document(metaclass=ExtendedType, mixin=True):
1715 """A mixin-class representing a unit test summary document (file)."""
1717 _path: Path #: Path to the YAML file.
1719 _analysisDuration: float #: TODO: replace by Timer; should be timedelta?
1720 _modelConversion: float #: TODO: replace by Timer; should be timedelta?
1722 def __init__(self, reportFile: Path, analyzeAndConvert: bool = False) -> None:
1723 self._path = reportFile
1725 self._analysisDuration = -1.0
1726 self._modelConversion = -1.0
1728 if analyzeAndConvert:
1729 self.Analyze()
1730 self.Convert()
1732 @readonly
1733 def Path(self) -> Path:
1734 """
1735 Read-only property to access the path to the file of this document.
1737 :returns: The document's path to the file.
1738 """
1739 return self._path
1741 @readonly
1742 def AnalysisDuration(self) -> timedelta:
1743 """
1744 Read-only property to return analysis duration.
1746 .. note::
1748 This includes usually the duration to validate and parse the file format, but it excludes the time to convert the
1749 content to the test entity hierarchy.
1751 :returns: Duration to analyze the document.
1752 """
1753 return timedelta(seconds=self._analysisDuration)
1755 @readonly
1756 def ModelConversionDuration(self) -> timedelta:
1757 """
1758 Read-only property to return conversion duration.
1760 .. note::
1762 This includes usually the duration to convert the document's content to the test entity hierarchy. It might also
1763 include the duration to (re-)aggregate all states and statistics in the hierarchy.
1765 :returns: Duration to convert the document.
1766 """
1767 return timedelta(seconds=self._modelConversion)
1769 @abstractmethod
1770 def Analyze(self) -> None:
1771 """Analyze and validate the document's content."""
1773 # @abstractmethod
1774 # def Write(self, path: Nullable[Path] = None, overwrite: bool = False):
1775 # pass
1777 @abstractmethod
1778 def Convert(self):
1779 """Convert the document's content to an instance of the test entity hierarchy."""
1782@export
1783class Merged(metaclass=ExtendedType, mixin=True):
1784 """A mixin-class representing a merged test entity."""
1786 _mergedCount: int
1788 def __init__(self, mergedCount: int = 1) -> None:
1789 self._mergedCount = mergedCount
1791 def _MergeStartTime(self, otherStartTime: Nullable[datetime]) -> Nullable[datetime]:
1792 """
1793 Combine this entity's start time with the start time of an entity being merged in.
1795 A merged entity started when the earliest of its parts started. An entity without a start time contributes
1796 nothing rather than erasing what is known.
1798 :param otherStartTime: The start time of the entity being merged in.
1799 :returns: The earlier of the two start times, or ``None`` if neither has one.
1800 """
1801 if otherStartTime is None:
1802 return self._startTime
1803 elif self._startTime is None or otherStartTime < self._startTime:
1804 return otherStartTime
1805 else:
1806 return self._startTime
1808 @readonly
1809 def MergedCount(self) -> int:
1810 """
1811 Read-only property to access how many source entities were merged into this one.
1813 :returns: Number of merged entities.
1814 """
1815 return self._mergedCount
1818@export
1819class Combined(metaclass=ExtendedType, mixin=True):
1820 _combinedCount: int
1822 def __init__(self, combinedCound: int = 1) -> None:
1823 self._combinedCount = combinedCound
1825 @readonly
1826 def CombinedCount(self) -> int:
1827 """
1828 Read-only property to access how many source entities were combined into this one.
1830 :returns: Number of combined entities.
1831 """
1832 return self._combinedCount
1835@export
1836class MergedTestcase(Testcase, Merged):
1837 _mergedTestcases: List[Testcase]
1839 def __init__(
1840 self,
1841 testcase: Testcase,
1842 parent: Nullable["Testsuite"] = None
1843 ) -> None:
1844 if testcase is None: 1844 ↛ 1845line 1844 didn't jump to line 1845 because the condition on line 1844 was never true
1845 raise ValueError(f"Parameter 'testcase' is None.")
1847 super().__init__(
1848 testcase._name,
1849 testcase._startTime,
1850 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration,
1851 TestcaseStatus.Unknown,
1852 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount,
1853 testcase._warningCount, testcase._errorCount, testcase._fatalCount,
1854 testcase._expectedWarningCount, testcase._expectedErrorCount, testcase._expectedFatalCount,
1855 parent=parent
1856 )
1857 Merged.__init__(self)
1859 self._mergedTestcases = [testcase]
1861 @readonly
1862 def Status(self) -> TestcaseStatus:
1863 """
1864 Read-only property to access the status merged from all source testcases.
1866 :returns: Merged status. ``TestcaseStatus.Inconsistent`` if the sources disagree.
1867 """
1868 if self._status is TestcaseStatus.Unknown: 1868 ↛ 1875line 1868 didn't jump to line 1875 because the condition on line 1868 was always true
1869 status = self._mergedTestcases[0]._status
1870 for mtc in self._mergedTestcases[1:]:
1871 status @= mtc._status
1873 self._status = status
1875 return self._status
1877 @readonly
1878 def SummedAssertionCount(self) -> int:
1879 """
1880 Read-only property to return the number of assertions across all merged testcases.
1882 :returns: Sum of the merged testcases' assertion counts.
1883 """
1884 return sum(tc._assertionCount for tc in self._mergedTestcases)
1886 @readonly
1887 def SummedPassedAssertionCount(self) -> int:
1888 """
1889 Read-only property to return the number of passed assertions across all merged testcases.
1891 :returns: Sum of the merged testcases' passed assertion counts.
1892 """
1893 return sum(tc._passedAssertionCount for tc in self._mergedTestcases)
1895 @readonly
1896 def SummedFailedAssertionCount(self) -> int:
1897 """
1898 Read-only property to return the number of failed assertions across all merged testcases.
1900 :returns: Sum of the merged testcases' failed assertion counts.
1901 """
1902 return sum(tc._failedAssertionCount for tc in self._mergedTestcases)
1904 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType:
1905 firstMTC = self._mergedTestcases[0]
1907 status = firstMTC._status
1908 warningCount = firstMTC._warningCount
1909 errorCount = firstMTC._errorCount
1910 fatalCount = firstMTC._fatalCount
1911 totalDuration = firstMTC._totalDuration
1913 for mtc in self._mergedTestcases[1:]:
1914 status @= mtc._status
1915 warningCount += mtc._warningCount
1916 errorCount += mtc._errorCount
1917 fatalCount += mtc._fatalCount
1919 self._status = status
1921 return warningCount, errorCount, fatalCount, self._expectedWarningCount, self._expectedErrorCount, self._expectedFatalCount, totalDuration
1923 def Merge(self, tc: Testcase) -> None:
1924 self._mergedCount += 1
1926 self._mergedTestcases.append(tc)
1928 self._warningCount += tc._warningCount
1929 self._errorCount += tc._errorCount
1930 self._fatalCount += tc._fatalCount
1932 def ToTestcase(self) -> Testcase:
1933 return Testcase(
1934 self._name,
1935 self._startTime,
1936 self._setupDuration,
1937 self._testDuration,
1938 self._teardownDuration,
1939 self._totalDuration,
1940 self._status,
1941 self._assertionCount,
1942 self._failedAssertionCount,
1943 self._passedAssertionCount,
1944 self._warningCount,
1945 self._errorCount,
1946 self._fatalCount
1947 )
1950@export
1951class MergedTestsuite(Testsuite, Merged):
1952 def __init__(
1953 self,
1954 testsuite: Testsuite,
1955 addTestsuites: bool = False,
1956 addTestcases: bool = False,
1957 parent: Nullable["Testsuite"] = None
1958 ) -> None:
1959 if testsuite is None: 1959 ↛ 1960line 1959 didn't jump to line 1960 because the condition on line 1959 was never true
1960 raise ValueError(f"Parameter 'testsuite' is None.")
1962 super().__init__(
1963 testsuite._name,
1964 testsuite._kind,
1965 testsuite._hostname,
1966 testsuite._startTime,
1967 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration,
1968 TestsuiteStatus.Unknown,
1969 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount,
1970 parent=parent
1971 )
1972 Merged.__init__(self)
1974 if addTestsuites: 1974 ↛ 1979line 1974 didn't jump to line 1979 because the condition on line 1974 was always true
1975 for ts in testsuite._testsuites.values():
1976 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases)
1977 self.AddTestsuite(mergedTestsuite)
1979 if addTestcases: 1979 ↛ exitline 1979 didn't return from function '__init__' because the condition on line 1979 was always true
1980 for tc in testsuite._testcases.values():
1981 mergedTestcase = MergedTestcase(tc)
1982 self.AddTestcase(mergedTestcase)
1984 def _MergeHostname(self, otherHostname: Nullable[str]) -> Nullable[str]:
1985 """
1986 Combine this test suite's hostname with the hostname of a test suite being merged in.
1988 A test suite without a hostname ran somewhere unrecorded, not somewhere else, so it keeps an otherwise
1989 unanimous hostname.
1991 :param otherHostname: The hostname of the test suite being merged in.
1992 :returns: The common hostname, ``"various"`` if they disagree, or ``None`` if neither has one.
1993 """
1994 if otherHostname is None:
1995 return self._hostname
1996 elif self._hostname is None or self._hostname == otherHostname:
1997 return otherHostname
1998 else:
1999 return "various"
2001 def Merge(self, testsuite: Testsuite) -> None:
2002 self._mergedCount += 1
2003 self._hostname = self._MergeHostname(testsuite._hostname)
2004 self._startTime = self._MergeStartTime(testsuite._startTime)
2006 for ts in testsuite._testsuites.values():
2007 if ts._name in self._testsuites: 2007 ↛ 2010line 2007 didn't jump to line 2010 because the condition on line 2007 was always true
2008 self._testsuites[ts._name].Merge(ts)
2009 else:
2010 mergedTestsuite = MergedTestsuite(ts, addTestsuites=True, addTestcases=True)
2011 self.AddTestsuite(mergedTestsuite)
2013 for tc in testsuite._testcases.values():
2014 if tc._name in self._testcases: 2014 ↛ 2017line 2014 didn't jump to line 2017 because the condition on line 2014 was always true
2015 self._testcases[tc._name].Merge(tc)
2016 else:
2017 mergedTestcase = MergedTestcase(tc)
2018 self.AddTestcase(mergedTestcase)
2020 def ToTestsuite(self) -> Testsuite:
2021 testsuite = Testsuite(
2022 self._name,
2023 self._kind,
2024 self._hostname,
2025 self._startTime,
2026 self._setupDuration,
2027 self._testDuration,
2028 self._teardownDuration,
2029 self._totalDuration,
2030 self._status,
2031 self._warningCount,
2032 self._errorCount,
2033 self._fatalCount,
2034 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values()),
2035 testcases=(tc.ToTestcase() for tc in self._testcases.values())
2036 )
2038 testsuite._tests = self._tests
2039 testsuite._excluded = self._excluded
2040 testsuite._inconsistent = self._inconsistent
2041 testsuite._skipped = self._skipped
2042 testsuite._errored = self._errored
2043 testsuite._weak = self._weak
2044 testsuite._failed = self._failed
2045 testsuite._passed = self._passed
2047 return testsuite
2050@export
2051class MergedTestsuiteSummary(TestsuiteSummary, Merged):
2052 _mergedFiles: Dict[Path, TestsuiteSummary]
2054 def __init__(self, name: str) -> None:
2055 super().__init__(name)
2056 Merged.__init__(self, mergedCount=0)
2058 self._mergedFiles = {}
2060 def Merge(self, testsuiteSummary: TestsuiteSummary) -> None:
2061 # if summary.File in self._mergedFiles:
2062 # raise
2064 # FIXME: a summary is not necessarily a file
2065 self._mergedCount += 1
2066 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary
2067 self._startTime = self._MergeStartTime(testsuiteSummary._startTime)
2069 for testsuite in testsuiteSummary._testsuites.values():
2070 if testsuite._name in self._testsuites:
2071 self._testsuites[testsuite._name].Merge(testsuite)
2072 else:
2073 mergedTestsuite = MergedTestsuite(testsuite, addTestsuites=True, addTestcases=True)
2074 self.AddTestsuite(mergedTestsuite)
2076 def ToTestsuiteSummary(self) -> TestsuiteSummary:
2077 testsuiteSummary = TestsuiteSummary(
2078 self._name,
2079 self._startTime,
2080 self._setupDuration,
2081 self._testDuration,
2082 self._teardownDuration,
2083 self._totalDuration,
2084 self._status,
2085 self._warningCount,
2086 self._errorCount,
2087 self._fatalCount,
2088 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values())
2089 )
2091 testsuiteSummary._tests = self._tests
2092 testsuiteSummary._excluded = self._excluded
2093 testsuiteSummary._inconsistent = self._inconsistent
2094 testsuiteSummary._skipped = self._skipped
2095 testsuiteSummary._errored = self._errored
2096 testsuiteSummary._weak = self._weak
2097 testsuiteSummary._failed = self._failed
2098 testsuiteSummary._passed = self._passed
2100 return testsuiteSummary