Coverage for pyEDAA / Reports / Unittesting / __init__.py: 78%
835 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-22 07:24 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-22 07:24 +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 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, int, int, int, timedelta]
248TestsuiteAggregateReturnType = Tuple[int, int, int, 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 _expectedWarningCount: int
287 _expectedErrorCount: int
288 _expectedFatalCount: int
290 _dict: Dict[str, Any]
292 def __init__(
293 self,
294 name: str,
295 startTime: Nullable[datetime] = None,
296 setupDuration: Nullable[timedelta] = None,
297 testDuration: Nullable[timedelta] = None,
298 teardownDuration: Nullable[timedelta] = None,
299 totalDuration: Nullable[timedelta] = None,
300 warningCount: int = 0,
301 errorCount: int = 0,
302 fatalCount: int = 0,
303 expectedWarningCount: int = 0,
304 expectedErrorCount: int = 0,
305 expectedFatalCount: int = 0,
306 keyValuePairs: Nullable[Mapping[str, Any]] = None,
307 parent: Nullable["TestsuiteBase"] = None
308 ) -> None:
309 """
310 Initializes the fields of the base-class.
312 :param name: Name of the test entity.
313 :param startTime: Time when the test entity was started.
314 :param setupDuration: Duration it took to set up the entity.
315 :param testDuration: Duration of the entity's test run.
316 :param teardownDuration: Duration it took to tear down the entity.
317 :param totalDuration: Total duration of the entity's execution (setup + test + teardown).
318 :param warningCount: Count of encountered warnings.
319 :param errorCount: Count of encountered errors.
320 :param fatalCount: Count of encountered fatal errors.
321 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with.
322 :param parent: Reference to the parent test entity.
323 :raises TypeError: When parameter 'parent' is not a TestsuiteBase.
324 :raises ValueError: When parameter 'name' is None.
325 :raises TypeError: When parameter 'name' is not a string.
326 :raises ValueError: When parameter 'name' is empty.
327 :raises TypeError: When parameter 'testDuration' is not a timedelta.
328 :raises TypeError: When parameter 'setupDuration' is not a timedelta.
329 :raises TypeError: When parameter 'teardownDuration' is not a timedelta.
330 :raises TypeError: When parameter 'totalDuration' is not a timedelta.
331 :raises TypeError: When parameter 'warningCount' is not an integer.
332 :raises TypeError: When parameter 'errorCount' is not an integer.
333 :raises TypeError: When parameter 'fatalCount' is not an integer.
334 :raises TypeError: When parameter 'expectedWarningCount' is not an integer.
335 :raises TypeError: When parameter 'expectedErrorCount' is not an integer.
336 :raises TypeError: When parameter 'expectedFatalCount' is not an integer.
337 :raises TypeError: When parameter 'keyValuePairs' is not a Mapping.
338 :raises ValueError: When parameter 'totalDuration' is not consistent.
339 """
341 if parent is not None and not isinstance(parent, TestsuiteBase): 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true
342 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
343 if version_info >= (3, 11): # pragma: no cover
344 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
345 raise ex
347 if name is None:
348 raise ValueError(f"Parameter 'name' is None.")
349 elif not isinstance(name, str): 349 ↛ 350line 349 didn't jump to line 350 because the condition on line 349 was never true
350 ex = TypeError(f"Parameter 'name' is not of type 'str'.")
351 if version_info >= (3, 11): # pragma: no cover
352 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
353 raise ex
354 elif name.strip() == "": 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true
355 raise ValueError(f"Parameter 'name' is empty.")
357 self._parent = parent
358 self._name = name
360 if testDuration is not None and not isinstance(testDuration, timedelta): 360 ↛ 361line 360 didn't jump to line 361 because the condition on line 360 was never true
361 ex = TypeError(f"Parameter 'testDuration' is not of type 'timedelta'.")
362 if version_info >= (3, 11): # pragma: no cover
363 ex.add_note(f"Got type '{getFullyQualifiedName(testDuration)}'.")
364 raise ex
366 if setupDuration is not None and not isinstance(setupDuration, timedelta): 366 ↛ 367line 366 didn't jump to line 367 because the condition on line 366 was never true
367 ex = TypeError(f"Parameter 'setupDuration' is not of type 'timedelta'.")
368 if version_info >= (3, 11): # pragma: no cover
369 ex.add_note(f"Got type '{getFullyQualifiedName(setupDuration)}'.")
370 raise ex
372 if teardownDuration is not None and not isinstance(teardownDuration, timedelta): 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true
373 ex = TypeError(f"Parameter 'teardownDuration' is not of type 'timedelta'.")
374 if version_info >= (3, 11): # pragma: no cover
375 ex.add_note(f"Got type '{getFullyQualifiedName(teardownDuration)}'.")
376 raise ex
378 if totalDuration is not None and not isinstance(totalDuration, timedelta): 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true
379 ex = TypeError(f"Parameter 'totalDuration' is not of type 'timedelta'.")
380 if version_info >= (3, 11): # pragma: no cover
381 ex.add_note(f"Got type '{getFullyQualifiedName(totalDuration)}'.")
382 raise ex
384 if testDuration is not None:
385 if setupDuration is not None: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true
386 if teardownDuration is not None:
387 if totalDuration is not None:
388 if totalDuration < (setupDuration + testDuration + teardownDuration):
389 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup, test and teardown durations.")
390 else: # no total
391 totalDuration = setupDuration + testDuration + teardownDuration
392 # no teardown
393 elif totalDuration is not None:
394 if totalDuration < (setupDuration + testDuration):
395 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup and test durations.")
396 # no teardown, no total
397 else:
398 totalDuration = setupDuration + testDuration
399 # no setup
400 elif teardownDuration is not None: 400 ↛ 401line 400 didn't jump to line 401 because the condition on line 400 was never true
401 if totalDuration is not None:
402 if totalDuration < (testDuration + teardownDuration):
403 raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of test and teardown durations.")
404 else: # no setup, no total
405 totalDuration = testDuration + teardownDuration
406 # no setup, no teardown
407 elif totalDuration is not None:
408 if totalDuration < testDuration: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 raise ValueError(f"Parameter 'totalDuration' can not be less than test durations.")
410 else: # no setup, no teardown, no total
411 totalDuration = testDuration
412 # no test
413 elif totalDuration is not None:
414 testDuration = totalDuration
415 if setupDuration is not None: 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true
416 testDuration -= setupDuration
417 if teardownDuration is not None: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true
418 testDuration -= teardownDuration
420 self._startTime = startTime
421 self._setupDuration = setupDuration
422 self._testDuration = testDuration
423 self._teardownDuration = teardownDuration
424 self._totalDuration = totalDuration
426 if not isinstance(warningCount, int): 426 ↛ 427line 426 didn't jump to line 427 because the condition on line 426 was never true
427 ex = TypeError(f"Parameter 'warningCount' is not of type 'int'.")
428 if version_info >= (3, 11): # pragma: no cover
429 ex.add_note(f"Got type '{getFullyQualifiedName(warningCount)}'.")
430 raise ex
432 if not isinstance(errorCount, int): 432 ↛ 433line 432 didn't jump to line 433 because the condition on line 432 was never true
433 ex = TypeError(f"Parameter 'errorCount' is not of type 'int'.")
434 if version_info >= (3, 11): # pragma: no cover
435 ex.add_note(f"Got type '{getFullyQualifiedName(errorCount)}'.")
436 raise ex
438 if not isinstance(fatalCount, int): 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true
439 ex = TypeError(f"Parameter 'fatalCount' is not of type 'int'.")
440 if version_info >= (3, 11): # pragma: no cover
441 ex.add_note(f"Got type '{getFullyQualifiedName(fatalCount)}'.")
442 raise ex
444 if not isinstance(expectedWarningCount, int): 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true
445 ex = TypeError(f"Parameter 'expectedWarningCount' is not of type 'int'.")
446 if version_info >= (3, 11): # pragma: no cover
447 ex.add_note(f"Got type '{getFullyQualifiedName(expectedWarningCount)}'.")
448 raise ex
450 if not isinstance(expectedErrorCount, int): 450 ↛ 451line 450 didn't jump to line 451 because the condition on line 450 was never true
451 ex = TypeError(f"Parameter 'expectedErrorCount' is not of type 'int'.")
452 if version_info >= (3, 11): # pragma: no cover
453 ex.add_note(f"Got type '{getFullyQualifiedName(expectedErrorCount)}'.")
454 raise ex
456 if not isinstance(expectedFatalCount, int): 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true
457 ex = TypeError(f"Parameter 'expectedFatalCount' is not of type 'int'.")
458 if version_info >= (3, 11): # pragma: no cover
459 ex.add_note(f"Got type '{getFullyQualifiedName(expectedFatalCount)}'.")
460 raise ex
462 self._warningCount = warningCount
463 self._errorCount = errorCount
464 self._fatalCount = fatalCount
465 self._expectedWarningCount = expectedWarningCount
466 self._expectedErrorCount = expectedErrorCount
467 self._expectedFatalCount = expectedFatalCount
469 if keyValuePairs is not None and not isinstance(keyValuePairs, Mapping): 469 ↛ 470line 469 didn't jump to line 470 because the condition on line 469 was never true
470 ex = TypeError(f"Parameter 'keyValuePairs' is not a mapping.")
471 if version_info >= (3, 11): # pragma: no cover
472 ex.add_note(f"Got type '{getFullyQualifiedName(keyValuePairs)}'.")
473 raise ex
475 self._dict = {} if keyValuePairs is None else {k: v for k, v in keyValuePairs}
477 # QUESTION: allow Parent as setter?
478 @readonly
479 def Parent(self) -> Nullable["TestsuiteBase"]:
480 """
481 Read-only property returning the reference to the parent test entity.
483 :returns: Reference to the parent entity.
484 """
485 return self._parent
487 @readonly
488 def Name(self) -> str:
489 """
490 Read-only property returning the test entity's name.
492 :returns: The test entities name.
493 """
494 return self._name
496 @readonly
497 def StartTime(self) -> Nullable[datetime]:
498 """
499 Read-only property returning the time when the test entity was started.
501 :returns: Time when the test entity was started.
502 """
503 return self._startTime
505 @readonly
506 def SetupDuration(self) -> Nullable[timedelta]:
507 """
508 Read-only property returning the duration of the test entity's setup.
510 :returns: Duration it took to set up the entity.
511 """
512 return self._setupDuration
514 @readonly
515 def TestDuration(self) -> Nullable[timedelta]:
516 """
517 Read-only property returning the duration of a test entities run.
519 This duration is excluding setup and teardown durations. In case setup and/or teardown durations are unknown or not
520 distinguishable, assign setup and teardown durations with zero.
522 :returns: Duration of the entity's test run.
523 """
524 return self._testDuration
526 @readonly
527 def TeardownDuration(self) -> Nullable[timedelta]:
528 """
529 Read-only property returning the duration of the test entity's teardown.
531 :returns: Duration it took to tear down the entity.
532 """
533 return self._teardownDuration
535 @readonly
536 def TotalDuration(self) -> Nullable[timedelta]:
537 """
538 Read-only property returning the total duration of a test entity run.
540 this duration includes setup and teardown durations.
542 :returns: Total duration of the entity's execution (setup + test + teardown)
543 """
544 return self._totalDuration
546 @readonly
547 def WarningCount(self) -> int:
548 """
549 Read-only property returning the number of encountered warnings.
551 :returns: Count of encountered warnings.
552 """
553 return self._warningCount
555 @readonly
556 def ErrorCount(self) -> int:
557 """
558 Read-only property returning the number of encountered errors.
560 :returns: Count of encountered errors.
561 """
562 return self._errorCount
564 @readonly
565 def FatalCount(self) -> int:
566 """
567 Read-only property returning the number of encountered fatal errors.
569 :returns: Count of encountered fatal errors.
570 """
571 return self._fatalCount
573 @readonly
574 def ExpectedWarningCount(self) -> int:
575 """
576 Read-only property returning the number of expected warnings.
578 :returns: Count of expected warnings.
579 """
580 return self._expectedWarningCount
582 @readonly
583 def ExpectedErrorCount(self) -> int:
584 """
585 Read-only property returning the number of expected errors.
587 :returns: Count of expected errors.
588 """
589 return self._expectedErrorCount
591 @readonly
592 def ExpectedFatalCount(self) -> int:
593 """
594 Read-only property returning the number of expected fatal errors.
596 :returns: Count of expected fatal errors.
597 """
598 return self._expectedFatalCount
600 def __len__(self) -> int:
601 """
602 Returns the number of annotated key-value pairs.
604 :returns: Number of annotated key-value pairs.
605 """
606 return len(self._dict)
608 def __getitem__(self, key: str) -> Any:
609 """
610 Access a key-value pair by key.
612 :param key: Name if the key-value pair.
613 :returns: Value of the accessed key.
614 """
615 return self._dict[key]
617 def __setitem__(self, key: str, value: Any) -> None:
618 """
619 Set the value of a key-value pair by key.
621 If the pair doesn't exist yet, it's created.
623 :param key: Key of the key-value pair.
624 :param value: Value of the key-value pair.
625 """
626 self._dict[key] = value
628 def __delitem__(self, key: str) -> None:
629 """
630 Delete a key-value pair by key.
632 :param key: Name if the key-value pair.
633 """
634 del self._dict[key]
636 def __contains__(self, key: str) -> bool:
637 """
638 Returns True, if a key-value pairs was annotated by this key.
640 :param key: Name of the key-value pair.
641 :returns: True, if the pair was annotated.
642 """
643 return key in self._dict
645 def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
646 """
647 Iterate all annotated key-value pairs.
649 :returns: A generator of key-value pair tuples (key, value).
650 """
651 yield from self._dict.items()
653 @abstractmethod
654 def Aggregate(self, strict: bool = True) -> None:
655 """
656 Aggregate all test entities in the hierarchy.
657 """
659 @abstractmethod
660 def __str__(self) -> str:
661 """
662 Formats the test entity as human-readable incl. some statistics.
663 """
666@export
667class Testcase(Base):
668 """
669 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run.
671 Test cases are grouped by test suites in the test entity hierarchy. The root of the hierarchy is a test summary.
673 Every test case has an overall status like unknown, skipped, failed or passed.
675 In addition to all features from its base-class, test cases provide additional statistics for passed and failed
676 assertions (checks) as well as a sum thereof.
677 """
679 _status: TestcaseStatus
680 _assertionCount: Nullable[int]
681 _failedAssertionCount: Nullable[int]
682 _passedAssertionCount: Nullable[int]
684 def __init__(
685 self,
686 name: str,
687 startTime: Nullable[datetime] = None,
688 setupDuration: Nullable[timedelta] = None,
689 testDuration: Nullable[timedelta] = None,
690 teardownDuration: Nullable[timedelta] = None,
691 totalDuration: Nullable[timedelta] = None,
692 status: TestcaseStatus = TestcaseStatus.Unknown,
693 assertionCount: Nullable[int] = None,
694 failedAssertionCount: Nullable[int] = None,
695 passedAssertionCount: Nullable[int] = None,
696 warningCount: int = 0,
697 errorCount: int = 0,
698 fatalCount: int = 0,
699 expectedWarningCount: int = 0,
700 expectedErrorCount: int = 0,
701 expectedFatalCount: int = 0,
702 keyValuePairs: Nullable[Mapping[str, Any]] = None,
703 parent: Nullable["Testsuite"] = None
704 ) -> None:
705 """
706 Initializes the fields of a test case.
708 :param name: Name of the test entity.
709 :param startTime: Time when the test entity was started.
710 :param setupDuration: Duration it took to set up the entity.
711 :param testDuration: Duration of the entity's test run.
712 :param teardownDuration: Duration it took to tear down the entity.
713 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
714 :param status: Status of the test case.
715 :param assertionCount: Number of assertions within the test.
716 :param failedAssertionCount: Number of failed assertions within the test.
717 :param passedAssertionCount: Number of passed assertions within the test.
718 :param warningCount: Count of encountered warnings.
719 :param errorCount: Count of encountered errors.
720 :param fatalCount: Count of encountered fatal errors.
721 :param keyValuePairs: Mapping of key-value pairs to initialize the test case.
722 :param parent: Reference to the parent test suite.
723 :raises TypeError: If parameter 'parent' is not a Testsuite.
724 :raises ValueError: If parameter 'assertionCount' is not consistent.
725 """
727 if parent is not None:
728 if not isinstance(parent, Testsuite):
729 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.")
730 if version_info >= (3, 11): # pragma: no cover
731 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
732 raise ex
734 parent._testcases[name] = self
736 super().__init__(
737 name,
738 startTime,
739 setupDuration, testDuration, teardownDuration, totalDuration,
740 warningCount, errorCount, fatalCount,
741 expectedWarningCount, expectedErrorCount, expectedFatalCount,
742 keyValuePairs,
743 parent
744 )
746 if not isinstance(status, TestcaseStatus): 746 ↛ 747line 746 didn't jump to line 747 because the condition on line 746 was never true
747 ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.")
748 if version_info >= (3, 11): # pragma: no cover
749 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.")
750 raise ex
752 self._status = status
754 if assertionCount is not None and not isinstance(assertionCount, int): 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true
755 ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.")
756 if version_info >= (3, 11): # pragma: no cover
757 ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.")
758 raise ex
760 if failedAssertionCount is not None and not isinstance(failedAssertionCount, int): 760 ↛ 761line 760 didn't jump to line 761 because the condition on line 760 was never true
761 ex = TypeError(f"Parameter 'failedAssertionCount' is not of type 'int'.")
762 if version_info >= (3, 11): # pragma: no cover
763 ex.add_note(f"Got type '{getFullyQualifiedName(failedAssertionCount)}'.")
764 raise ex
766 if passedAssertionCount is not None and not isinstance(passedAssertionCount, int): 766 ↛ 767line 766 didn't jump to line 767 because the condition on line 766 was never true
767 ex = TypeError(f"Parameter 'passedAssertionCount' is not of type 'int'.")
768 if version_info >= (3, 11): # pragma: no cover
769 ex.add_note(f"Got type '{getFullyQualifiedName(passedAssertionCount)}'.")
770 raise ex
772 self._assertionCount = assertionCount
773 if assertionCount is not None:
774 if failedAssertionCount is not None:
775 self._failedAssertionCount = failedAssertionCount
777 if passedAssertionCount is not None:
778 if passedAssertionCount + failedAssertionCount != assertionCount:
779 raise ValueError(f"passed assertion count ({passedAssertionCount}) + failed assertion count ({failedAssertionCount} != assertion count ({assertionCount}")
781 self._passedAssertionCount = passedAssertionCount
782 else:
783 self._passedAssertionCount = assertionCount - failedAssertionCount
784 elif passedAssertionCount is not None:
785 self._passedAssertionCount = passedAssertionCount
786 self._failedAssertionCount = assertionCount - passedAssertionCount
787 else:
788 raise ValueError(f"Neither passed assertion count nor failed assertion count are provided.")
789 elif failedAssertionCount is not None:
790 self._failedAssertionCount = failedAssertionCount
792 if passedAssertionCount is not None:
793 self._passedAssertionCount = passedAssertionCount
794 self._assertionCount = passedAssertionCount + failedAssertionCount
795 else:
796 raise ValueError(f"Passed assertion count is mandatory, if failed assertion count is provided instead of assertion count.")
797 elif passedAssertionCount is not None:
798 raise ValueError(f"Assertion count or failed assertion count is mandatory, if passed assertion count is provided.")
799 else:
800 self._passedAssertionCount = None
801 self._failedAssertionCount = None
803 @readonly
804 def Status(self) -> TestcaseStatus:
805 """
806 Read-only property returning the status of the test case.
808 :returns: The test case's status.
809 """
810 return self._status
812 @readonly
813 def AssertionCount(self) -> int:
814 """
815 Read-only property returning the number of assertions (checks) in a test case.
817 :returns: Number of assertions.
818 """
819 if self._assertionCount is None:
820 return 0
821 return self._assertionCount
823 @readonly
824 def FailedAssertionCount(self) -> int:
825 """
826 Read-only property returning the number of failed assertions (failed checks) in a test case.
828 :returns: Number of assertions.
829 """
830 return self._failedAssertionCount
832 @readonly
833 def PassedAssertionCount(self) -> int:
834 """
835 Read-only property returning the number of passed assertions (successful checks) in a test case.
837 :returns: Number of passed assertions.
838 """
839 return self._passedAssertionCount
841 def Copy(self) -> "Testcase":
842 return self.__class__(
843 self._name,
844 self._startTime,
845 self._setupDuration,
846 self._testDuration,
847 self._teardownDuration,
848 self._totalDuration,
849 self._status,
850 self._warningCount,
851 self._errorCount,
852 self._fatalCount,
853 self._expectedWarningCount,
854 self._expectedErrorCount,
855 self._expectedFatalCount,
856 )
857 # TODO: copy key-value-pairs?
859 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType:
860 if self._status is TestcaseStatus.Unknown: 860 ↛ 885line 860 didn't jump to line 885 because the condition on line 860 was always true
861 if self._assertionCount is None: 861 ↛ 862line 861 didn't jump to line 862 because the condition on line 861 was never true
862 self._status = TestcaseStatus.Passed
863 elif self._assertionCount == 0: 863 ↛ 864line 863 didn't jump to line 864 because the condition on line 863 was never true
864 self._status = TestcaseStatus.Weak
865 elif self._failedAssertionCount == 0:
866 self._status = TestcaseStatus.Passed
867 else:
868 self._status = TestcaseStatus.Failed
870 if self._warningCount - self._expectedWarningCount > 0: 870 ↛ 871line 870 didn't jump to line 871 because the condition on line 870 was never true
871 self._status |= TestcaseStatus.Warned
873 if self._errorCount - self._expectedErrorCount > 0: 873 ↛ 874line 873 didn't jump to line 874 because the condition on line 873 was never true
874 self._status |= TestcaseStatus.Errored
876 if self._fatalCount - self._expectedFatalCount > 0: 876 ↛ 877line 876 didn't jump to line 877 because the condition on line 876 was never true
877 self._status |= TestcaseStatus.Aborted
879 if strict:
880 self._status = self._status & ~TestcaseStatus.Passed | TestcaseStatus.Failed
882 # TODO: check for setup errors
883 # TODO: check for teardown errors
885 totalDuration = timedelta() if self._totalDuration is None else self._totalDuration
887 return self._warningCount, self._errorCount, self._fatalCount, self._expectedWarningCount, self._expectedErrorCount, self._expectedFatalCount, totalDuration
889 def __str__(self) -> str:
890 """
891 Formats the test case as human-readable incl. statistics.
893 :pycode:`f"<Testcase {}: {} - assert/pass/fail:{}/{}/{} - warn/error/fatal:{}/{}/{} - setup/test/teardown:{}/{}/{}>"`
895 :returns: Human-readable summary of a test case object.
896 """
897 return (
898 f"<Testcase {self._name}: {self._status.name} -"
899 f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
900 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount} -"
901 f" setup/test/teardown:{self._setupDuration:.3f}/{self._testDuration:.3f}/{self._teardownDuration:.3f}>"
902 )
905@export
906class TestsuiteBase(Base, Generic[TestsuiteType]):
907 """
908 Base-class for all test suites and for test summaries.
910 A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root
911 element in that hierarchy. While a test suite groups other test suites and test cases, a test summary can only group
912 test suites. Thus, a test summary contains no test cases.
913 """
915 _kind: TestsuiteKind
916 _status: TestsuiteStatus
917 _testsuites: Dict[str, TestsuiteType]
919 _tests: int
920 _inconsistent: int
921 _excluded: int
922 _skipped: int
923 _errored: int
924 _weak: int
925 _failed: int
926 _passed: int
928 def __init__(
929 self,
930 name: str,
931 kind: TestsuiteKind = TestsuiteKind.Logical,
932 startTime: Nullable[datetime] = None,
933 setupDuration: Nullable[timedelta] = None,
934 testDuration: Nullable[timedelta] = None,
935 teardownDuration: Nullable[timedelta] = None,
936 totalDuration: Nullable[timedelta] = None,
937 status: TestsuiteStatus = TestsuiteStatus.Unknown,
938 warningCount: int = 0,
939 errorCount: int = 0,
940 fatalCount: int = 0,
941 testsuites: Nullable[Iterable[TestsuiteType]] = None,
942 keyValuePairs: Nullable[Mapping[str, Any]] = None,
943 parent: Nullable["Testsuite"] = None
944 ) -> None:
945 """
946 Initializes the based-class fields of a test suite or test summary.
948 :param name: Name of the test entity.
949 :param kind: Kind of the test entity.
950 :param startTime: Time when the test entity was started.
951 :param setupDuration: Duration it took to set up the entity.
952 :param testDuration: Duration of all tests listed in the test entity.
953 :param teardownDuration: Duration it took to tear down the entity.
954 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
955 :param status: Overall status of the test entity.
956 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
957 :param errorCount: Count of encountered errors incl. errors from sub-elements.
958 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
959 :param testsuites: List of test suites to initialize the test entity with.
960 :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with.
961 :param parent: Reference to the parent test entity.
962 :raises TypeError: If parameter 'parent' is not a TestsuiteBase.
963 :raises TypeError: If parameter 'testsuites' is not iterable.
964 :raises TypeError: If element in parameter 'testsuites' is not a Testsuite.
965 :raises AlreadyInHierarchyException: If a test suite in parameter 'testsuites' is already part of a test entity hierarchy.
966 :raises DuplicateTestsuiteException: If a test suite in parameter 'testsuites' is already listed (by name) in the list of test suites.
967 """
968 if parent is not None:
969 if not isinstance(parent, TestsuiteBase): 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true
970 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
971 if version_info >= (3, 11): # pragma: no cover
972 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
973 raise ex
975 parent._testsuites[name] = self
977 super().__init__(
978 name,
979 startTime,
980 setupDuration,
981 testDuration,
982 teardownDuration,
983 totalDuration,
984 warningCount,
985 errorCount,
986 fatalCount,
987 0, 0, 0,
988 keyValuePairs,
989 parent
990 )
992 self._kind = kind
993 self._status = status
995 self._testsuites = {}
996 if testsuites is not None:
997 if not isinstance(testsuites, Iterable): 997 ↛ 998line 997 didn't jump to line 998 because the condition on line 997 was never true
998 ex = TypeError(f"Parameter 'testsuites' is not iterable.")
999 if version_info >= (3, 11): # pragma: no cover
1000 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.")
1001 raise ex
1003 for testsuite in testsuites:
1004 if not isinstance(testsuite, Testsuite): 1004 ↛ 1005line 1004 didn't jump to line 1005 because the condition on line 1004 was never true
1005 ex = TypeError(f"Element of parameter 'testsuites' is not of type 'Testsuite'.")
1006 if version_info >= (3, 11): # pragma: no cover
1007 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.")
1008 raise ex
1010 if testsuite._parent is not None: 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true
1011 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1013 if testsuite._name in self._testsuites:
1014 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1016 testsuite._parent = self
1017 self._testsuites[testsuite._name] = testsuite
1019 self._status = TestsuiteStatus.Unknown
1020 self._tests = 0
1021 self._inconsistent = 0
1022 self._excluded = 0
1023 self._skipped = 0
1024 self._errored = 0
1025 self._weak = 0
1026 self._failed = 0
1027 self._passed = 0
1029 @readonly
1030 def Kind(self) -> TestsuiteKind:
1031 """
1032 Read-only property returning the kind of the test suite.
1034 Test suites are used to group test cases. This grouping can be due to language/framework specifics like tests
1035 grouped by a module file or namespace. Others might be just logically grouped without any relation to a programming
1036 language construct.
1038 Test summaries always return kind ``Root``.
1040 :returns: Kind of the test suite.
1041 """
1042 return self._kind
1044 @readonly
1045 def Status(self) -> TestsuiteStatus:
1046 """
1047 Read-only property returning the aggregated overall status of the test suite.
1049 :returns: Overall status of the test suite.
1050 """
1051 return self._status
1053 @readonly
1054 def Testsuites(self) -> Dict[str, TestsuiteType]:
1055 """
1056 Read-only property returning a reference to the internal dictionary of test suites.
1058 :returns: Reference to the dictionary of test suite.
1059 """
1060 return self._testsuites
1062 @readonly
1063 def TestsuiteCount(self) -> int:
1064 """
1065 Read-only property returning the number of all test suites in the test suite hierarchy.
1067 :returns: Number of test suites.
1068 """
1069 return 1 + sum(testsuite.TestsuiteCount for testsuite in self._testsuites.values())
1071 @readonly
1072 def TestcaseCount(self) -> int:
1073 """
1074 Read-only property returning the number of all test cases in the test entity hierarchy.
1076 :returns: Number of test cases.
1077 """
1078 return sum(testsuite.TestcaseCount for testsuite in self._testsuites.values())
1080 @readonly
1081 def AssertionCount(self) -> int:
1082 """
1083 Read-only property returning the number of all assertions in all test cases in the test entity hierarchy.
1085 :returns: Number of assertions in all test cases.
1086 """
1087 return sum(ts.AssertionCount for ts in self._testsuites.values())
1089 @readonly
1090 def FailedAssertionCount(self) -> int:
1091 """
1092 Read-only property returning the number of all failed assertions in all test cases in the test entity hierarchy.
1094 :returns: Number of failed assertions in all test cases.
1095 """
1096 raise NotImplementedError()
1097 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount)
1099 @readonly
1100 def PassedAssertionCount(self) -> int:
1101 """
1102 Read-only property returning the number of all passed assertions in all test cases in the test entity hierarchy.
1104 :returns: Number of passed assertions in all test cases.
1105 """
1106 raise NotImplementedError()
1107 # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount)
1109 @readonly
1110 def Tests(self) -> int:
1111 return self._tests
1113 @readonly
1114 def Inconsistent(self) -> int:
1115 """
1116 Read-only property returning the number of inconsistent tests in the test suite hierarchy.
1118 :returns: Number of inconsistent tests.
1119 """
1120 return self._inconsistent
1122 @readonly
1123 def Excluded(self) -> int:
1124 """
1125 Read-only property returning the number of excluded tests in the test suite hierarchy.
1127 :returns: Number of excluded tests.
1128 """
1129 return self._excluded
1131 @readonly
1132 def Skipped(self) -> int:
1133 """
1134 Read-only property returning the number of skipped tests in the test suite hierarchy.
1136 :returns: Number of skipped tests.
1137 """
1138 return self._skipped
1140 @readonly
1141 def Errored(self) -> int:
1142 """
1143 Read-only property returning the number of tests with errors in the test suite hierarchy.
1145 :returns: Number of errored tests.
1146 """
1147 return self._errored
1149 @readonly
1150 def Weak(self) -> int:
1151 """
1152 Read-only property returning the number of weak tests in the test suite hierarchy.
1154 :returns: Number of weak tests.
1155 """
1156 return self._weak
1158 @readonly
1159 def Failed(self) -> int:
1160 """
1161 Read-only property returning the number of failed tests in the test suite hierarchy.
1163 :returns: Number of failed tests.
1164 """
1165 return self._failed
1167 @readonly
1168 def Passed(self) -> int:
1169 """
1170 Read-only property returning the number of passed tests in the test suite hierarchy.
1172 :returns: Number of passed tests.
1173 """
1174 return self._passed
1176 @readonly
1177 def WarningCount(self) -> int:
1178 raise NotImplementedError()
1179 # return self._warningCount
1181 @readonly
1182 def ErrorCount(self) -> int:
1183 raise NotImplementedError()
1184 # return self._errorCount
1186 @readonly
1187 def FatalCount(self) -> int:
1188 raise NotImplementedError()
1189 # return self._fatalCount
1191 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1192 tests = 0
1193 inconsistent = 0
1194 excluded = 0
1195 skipped = 0
1196 errored = 0
1197 weak = 0
1198 failed = 0
1199 passed = 0
1201 warningCount = 0
1202 errorCount = 0
1203 fatalCount = 0
1205 expectedWarningCount = 0
1206 expectedErrorCount = 0
1207 expectedFatalCount = 0
1209 totalDuration = timedelta()
1211 for testsuite in self._testsuites.values():
1212 t, i, ex, s, e, w, f, p, wc, ec, fc, ewc, eec, efc, td = testsuite.Aggregate(strict)
1213 tests += t
1214 inconsistent += i
1215 excluded += ex
1216 skipped += s
1217 errored += e
1218 weak += w
1219 failed += f
1220 passed += p
1222 warningCount += wc
1223 errorCount += ec
1224 fatalCount += fc
1226 expectedWarningCount += ewc
1227 expectedErrorCount += eec
1228 expectedFatalCount += efc
1230 totalDuration += td
1232 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration
1234 def AddTestsuite(self, testsuite: TestsuiteType) -> None:
1235 """
1236 Add a test suite to the list of test suites.
1238 :param testsuite: The test suite to add.
1239 :raises ValueError: If parameter 'testsuite' is None.
1240 :raises TypeError: If parameter 'testsuite' is not a Testsuite.
1241 :raises AlreadyInHierarchyException: If parameter 'testsuite' is already part of a test entity hierarchy.
1242 :raises DuplicateTestcaseException: If parameter 'testsuite' is already listed (by name) in the list of test suites.
1243 """
1244 if testsuite is None: 1244 ↛ 1245line 1244 didn't jump to line 1245 because the condition on line 1244 was never true
1245 raise ValueError("Parameter 'testsuite' is None.")
1246 elif not isinstance(testsuite, Testsuite): 1246 ↛ 1247line 1246 didn't jump to line 1247 because the condition on line 1246 was never true
1247 ex = TypeError(f"Parameter 'testsuite' is not of type 'Testsuite'.")
1248 if version_info >= (3, 11): # pragma: no cover
1249 ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.")
1250 raise ex
1252 if testsuite._parent is not None: 1252 ↛ 1253line 1252 didn't jump to line 1253 because the condition on line 1252 was never true
1253 raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1255 if testsuite._name in self._testsuites:
1256 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1258 testsuite._parent = self
1259 self._testsuites[testsuite._name] = testsuite
1261 def AddTestsuites(self, testsuites: Iterable[TestsuiteType]) -> None:
1262 """
1263 Add a list of test suites to the list of test suites.
1265 :param testsuites: List of test suites to add.
1266 :raises ValueError: If parameter 'testsuites' is None.
1267 :raises TypeError: If parameter 'testsuites' is not iterable.
1268 """
1269 if testsuites is None: 1269 ↛ 1270line 1269 didn't jump to line 1270 because the condition on line 1269 was never true
1270 raise ValueError("Parameter 'testsuites' is None.")
1271 elif not isinstance(testsuites, Iterable): 1271 ↛ 1272line 1271 didn't jump to line 1272 because the condition on line 1271 was never true
1272 ex = TypeError(f"Parameter 'testsuites' is not iterable.")
1273 if version_info >= (3, 11): # pragma: no cover
1274 ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.")
1275 raise ex
1277 for testsuite in testsuites:
1278 self.AddTestsuite(testsuite)
1280 @abstractmethod
1281 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1282 pass
1284 def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
1285 return self.Iterate(scheme)
1287 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
1288 return self.Iterate(scheme)
1290 def ToTree(self) -> Node:
1291 rootNode = Node(value=self._name)
1293 def convertTestcase(testcase: Testcase, parentNode: Node) -> None:
1294 _ = Node(value=testcase._name, parent=parentNode)
1296 def convertTestsuite(testsuite: Testsuite, parentNode: Node) -> None:
1297 testsuiteNode = Node(value=testsuite._name, parent=parentNode)
1299 for ts in testsuite._testsuites.values():
1300 convertTestsuite(ts, testsuiteNode)
1302 for tc in testsuite._testcases.values():
1303 convertTestcase(tc, testsuiteNode)
1305 for testsuite in self._testsuites.values():
1306 convertTestsuite(testsuite, rootNode)
1308 return rootNode
1311@export
1312class Testsuite(TestsuiteBase[TestsuiteType]):
1313 """
1314 A testsuite is a mid-level element in the test entity hierarchy representing a group of tests.
1316 Test suites contain test cases and optionally other test suites. Test suites can be grouped by test suites to form a
1317 hierarchy of test entities. The root of the hierarchy is a test summary.
1318 """
1320 _testcases: Dict[str, "Testcase"]
1322 def __init__(
1323 self,
1324 name: str,
1325 kind: TestsuiteKind = TestsuiteKind.Logical,
1326 startTime: Nullable[datetime] = None,
1327 setupDuration: Nullable[timedelta] = None,
1328 testDuration: Nullable[timedelta] = None,
1329 teardownDuration: Nullable[timedelta] = None,
1330 totalDuration: Nullable[timedelta] = None,
1331 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1332 warningCount: int = 0,
1333 errorCount: int = 0,
1334 fatalCount: int = 0,
1335 testsuites: Nullable[Iterable[TestsuiteType]] = None,
1336 testcases: Nullable[Iterable["Testcase"]] = None,
1337 keyValuePairs: Nullable[Mapping[str, Any]] = None,
1338 parent: Nullable[TestsuiteType] = None
1339 ) -> None:
1340 """
1341 Initializes the fields of a test suite.
1343 :param name: Name of the test suite.
1344 :param kind: Kind of the test suite.
1345 :param startTime: Time when the test suite was started.
1346 :param setupDuration: Duration it took to set up the test suite.
1347 :param testDuration: Duration of all tests listed in the test suite.
1348 :param teardownDuration: Duration it took to tear down the test suite.
1349 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
1350 :param status: Overall status of the test suite.
1351 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
1352 :param errorCount: Count of encountered errors incl. errors from sub-elements.
1353 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
1354 :param testsuites: List of test suites to initialize the test suite with.
1355 :param testcases: List of test cases to initialize the test suite with.
1356 :param keyValuePairs: Mapping of key-value pairs to initialize the test suite with.
1357 :param parent: Reference to the parent test entity.
1358 :raises TypeError: If parameter 'testcases' is not iterable.
1359 :raises TypeError: If element in parameter 'testcases' is not a Testcase.
1360 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy.
1361 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases.
1362 """
1363 super().__init__(
1364 name,
1365 kind,
1366 startTime,
1367 setupDuration,
1368 testDuration,
1369 teardownDuration,
1370 totalDuration,
1371 status,
1372 warningCount,
1373 errorCount,
1374 fatalCount,
1375 testsuites,
1376 keyValuePairs,
1377 parent
1378 )
1380 # self._testDuration = testDuration
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 if version_info >= (3, 11): # pragma: no cover
1387 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.")
1388 raise ex
1390 for testcase in testcases:
1391 if not isinstance(testcase, Testcase): 1391 ↛ 1392line 1391 didn't jump to line 1392 because the condition on line 1391 was never true
1392 ex = TypeError(f"Element of parameter 'testcases' is not of type 'Testcase'.")
1393 if version_info >= (3, 11): # pragma: no cover
1394 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.")
1395 raise ex
1397 if testcase._parent is not None: 1397 ↛ 1398line 1397 didn't jump to line 1398 because the condition on line 1397 was never true
1398 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
1400 if testcase._name in self._testcases:
1401 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1403 testcase._parent = self
1404 self._testcases[testcase._name] = testcase
1406 @readonly
1407 def Testcases(self) -> Dict[str, "Testcase"]:
1408 """
1409 Read-only property returning a reference to the internal dictionary of test cases.
1411 :returns: Reference to the dictionary of test cases.
1412 """
1413 return self._testcases
1415 @readonly
1416 def TestcaseCount(self) -> int:
1417 """
1418 Read-only property returning the number of all test cases in the test entity hierarchy.
1420 :returns: Number of test cases.
1421 """
1422 return super().TestcaseCount + len(self._testcases)
1424 @readonly
1425 def AssertionCount(self) -> int:
1426 return super().AssertionCount + sum(tc.AssertionCount for tc in self._testcases.values())
1428 def Copy(self) -> "Testsuite":
1429 return self.__class__(
1430 self._name,
1431 self._startTime,
1432 self._setupDuration,
1433 self._teardownDuration,
1434 self._totalDuration,
1435 self._status,
1436 self._warningCount,
1437 self._errorCount,
1438 self._fatalCount
1439 )
1441 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1442 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration = super().Aggregate()
1444 for testcase in self._testcases.values():
1445 wc, ec, fc, ewc, eec, efc, td = testcase.Aggregate(strict)
1447 tests += 1
1449 warningCount += wc
1450 errorCount += ec
1451 fatalCount += fc
1453 expectedWarningCount += ewc
1454 expectedErrorCount += eec
1455 expectedFatalCount += efc
1457 totalDuration += td
1459 status = testcase._status
1460 if status is TestcaseStatus.Unknown: 1460 ↛ 1461line 1460 didn't jump to line 1461 because the condition on line 1460 was never true
1461 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.")
1462 elif TestcaseStatus.Inconsistent in status: 1462 ↛ 1463line 1462 didn't jump to line 1463 because the condition on line 1462 was never true
1463 inconsistent += 1
1464 elif status is TestcaseStatus.Excluded: 1464 ↛ 1465line 1464 didn't jump to line 1465 because the condition on line 1464 was never true
1465 excluded += 1
1466 elif status is TestcaseStatus.Skipped:
1467 skipped += 1
1468 elif status is TestcaseStatus.Errored: 1468 ↛ 1469line 1468 didn't jump to line 1469 because the condition on line 1468 was never true
1469 errored += 1
1470 elif status is TestcaseStatus.Weak: 1470 ↛ 1471line 1470 didn't jump to line 1471 because the condition on line 1470 was never true
1471 weak += 1
1472 elif status is TestcaseStatus.Passed:
1473 passed += 1
1474 elif status is TestcaseStatus.Failed: 1474 ↛ 1476line 1474 didn't jump to line 1476 because the condition on line 1474 was always true
1475 failed += 1
1476 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown:
1477 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.")
1478 else:
1479 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.")
1481 self._tests = tests
1482 self._inconsistent = inconsistent
1483 self._excluded = excluded
1484 self._skipped = skipped
1485 self._errored = errored
1486 self._weak = weak
1487 self._failed = failed
1488 self._passed = passed
1490 self._warningCount = warningCount
1491 self._errorCount = errorCount
1492 self._fatalCount = fatalCount
1494 self._expectedWarningCount = expectedWarningCount
1495 self._expectedErrorCount = expectedErrorCount
1496 self._expectedFatalCount = expectedFatalCount
1498 if self._totalDuration is None:
1499 self._totalDuration = totalDuration
1501 if errored > 0: 1501 ↛ 1502line 1501 didn't jump to line 1502 because the condition on line 1501 was never true
1502 self._status = TestsuiteStatus.Errored
1503 elif failed > 0:
1504 self._status = TestsuiteStatus.Failed
1505 elif tests == 0: 1505 ↛ 1506line 1505 didn't jump to line 1506 because the condition on line 1505 was never true
1506 self._status = TestsuiteStatus.Empty
1507 elif tests - skipped == passed: 1507 ↛ 1509line 1507 didn't jump to line 1509 because the condition on line 1507 was always true
1508 self._status = TestsuiteStatus.Passed
1509 elif tests == skipped:
1510 self._status = TestsuiteStatus.Skipped
1511 else:
1512 self._status = TestsuiteStatus.Unknown
1514 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration
1516 def AddTestcase(self, testcase: "Testcase") -> None:
1517 """
1518 Add a test case to the list of test cases.
1520 :param testcase: The test case to add.
1521 :raises ValueError: If parameter 'testcase' is None.
1522 :raises TypeError: If parameter 'testcase' is not a Testcase.
1523 :raises AlreadyInHierarchyException: If parameter 'testcase' is already part of a test entity hierarchy.
1524 :raises DuplicateTestcaseException: If parameter 'testcase' is already listed (by name) in the list of test cases.
1525 """
1526 if testcase is None: 1526 ↛ 1527line 1526 didn't jump to line 1527 because the condition on line 1526 was never true
1527 raise ValueError("Parameter 'testcase' is None.")
1528 elif not isinstance(testcase, Testcase): 1528 ↛ 1529line 1528 didn't jump to line 1529 because the condition on line 1528 was never true
1529 ex = TypeError(f"Parameter 'testcase' is not of type 'Testcase'.")
1530 if version_info >= (3, 11): # pragma: no cover
1531 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.")
1532 raise ex
1534 if testcase._parent is not None: 1534 ↛ 1535line 1534 didn't jump to line 1535 because the condition on line 1534 was never true
1535 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
1537 if testcase._name in self._testcases:
1538 raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1540 testcase._parent = self
1541 self._testcases[testcase._name] = testcase
1543 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None:
1544 """
1545 Add a list of test cases to the list of test cases.
1547 :param testcases: List of test cases to add.
1548 :raises ValueError: If parameter 'testcases' is None.
1549 :raises TypeError: If parameter 'testcases' is not iterable.
1550 """
1551 if testcases is None: 1551 ↛ 1552line 1551 didn't jump to line 1552 because the condition on line 1551 was never true
1552 raise ValueError("Parameter 'testcases' is None.")
1553 elif not isinstance(testcases, Iterable): 1553 ↛ 1554line 1553 didn't jump to line 1554 because the condition on line 1553 was never true
1554 ex = TypeError(f"Parameter 'testcases' is not iterable.")
1555 if version_info >= (3, 11): # pragma: no cover
1556 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.")
1557 raise ex
1559 for testcase in testcases:
1560 self.AddTestcase(testcase)
1562 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1563 if IterationScheme.PreOrder in scheme:
1564 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1565 yield self
1567 if IterationScheme.IncludeTestcases in scheme:
1568 for testcase in self._testcases.values():
1569 yield testcase
1571 for testsuite in self._testsuites.values():
1572 yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf)
1574 if IterationScheme.PostOrder in scheme:
1575 if IterationScheme.IncludeTestcases in scheme:
1576 for testcase in self._testcases.values():
1577 yield testcase
1579 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1580 yield self
1582 def __str__(self) -> str:
1583 return (
1584 f"<Testsuite {self._name}: {self._status.name} -"
1585 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
1586 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>"
1587 )
1590@export
1591class TestsuiteSummary(TestsuiteBase[TestsuiteType]):
1592 """
1593 A testsuite summary is the root element in the test entity hierarchy representing a summary of all test suites and cases.
1595 The testsuite summary contains test suites, which in turn can contain test suites and test cases.
1596 """
1598 def __init__(
1599 self,
1600 name: str,
1601 startTime: Nullable[datetime] = None,
1602 setupDuration: Nullable[timedelta] = None,
1603 testDuration: Nullable[timedelta] = None,
1604 teardownDuration: Nullable[timedelta] = None,
1605 totalDuration: Nullable[timedelta] = None,
1606 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1607 warningCount: int = 0,
1608 errorCount: int = 0,
1609 fatalCount: int = 0,
1610 testsuites: Nullable[Iterable[TestsuiteType]] = None,
1611 keyValuePairs: Nullable[Mapping[str, Any]] = None,
1612 parent: Nullable[TestsuiteType] = None
1613 ) -> None:
1614 """
1615 Initializes the fields of a test summary.
1617 :param name: Name of the test summary.
1618 :param startTime: Time when the test summary was started.
1619 :param setupDuration: Duration it took to set up the test summary.
1620 :param testDuration: Duration of all tests listed in the test summary.
1621 :param teardownDuration: Duration it took to tear down the test summary.
1622 :param totalDuration: Total duration of the entity's execution (setup + test + teardown)
1623 :param status: Overall status of the test summary.
1624 :param warningCount: Count of encountered warnings incl. warnings from sub-elements.
1625 :param errorCount: Count of encountered errors incl. errors from sub-elements.
1626 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements.
1627 :param testsuites: List of test suites to initialize the test summary with.
1628 :param keyValuePairs: Mapping of key-value pairs to initialize the test summary with.
1629 :param parent: Reference to the parent test summary.
1630 """
1631 super().__init__(
1632 name,
1633 TestsuiteKind.Root,
1634 startTime, setupDuration, testDuration, teardownDuration, totalDuration,
1635 status,
1636 warningCount, errorCount, fatalCount,
1637 testsuites,
1638 keyValuePairs,
1639 parent
1640 )
1642 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1643 tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, expectedWarningCount, expectedErrorCount, expectedFatalCount, totalDuration = super().Aggregate(strict)
1645 self._tests = tests
1646 self._inconsistent = inconsistent
1647 self._excluded = excluded
1648 self._skipped = skipped
1649 self._errored = errored
1650 self._weak = weak
1651 self._failed = failed
1652 self._passed = passed
1654 self._warningCount = warningCount
1655 self._errorCount = errorCount
1656 self._fatalCount = fatalCount
1658 self._expectedWarningCount = expectedWarningCount
1659 self._expectedErrorCount = expectedErrorCount
1660 self._expectedFatalCount = expectedFatalCount
1662 if self._totalDuration is None: 1662 ↛ 1665line 1662 didn't jump to line 1665 because the condition on line 1662 was always true
1663 self._totalDuration = totalDuration
1665 if errored > 0: 1665 ↛ 1666line 1665 didn't jump to line 1666 because the condition on line 1665 was never true
1666 self._status = TestsuiteStatus.Errored
1667 elif failed > 0:
1668 self._status = TestsuiteStatus.Failed
1669 elif tests == 0: 1669 ↛ 1670line 1669 didn't jump to line 1670 because the condition on line 1669 was never true
1670 self._status = TestsuiteStatus.Empty
1671 elif tests - skipped == passed: 1671 ↛ 1673line 1671 didn't jump to line 1673 because the condition on line 1671 was always true
1672 self._status = TestsuiteStatus.Passed
1673 elif tests == skipped:
1674 self._status = TestsuiteStatus.Skipped
1675 elif tests == excluded:
1676 self._status = TestsuiteStatus.Excluded
1677 else:
1678 self._status = TestsuiteStatus.Unknown
1680 return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration
1682 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
1683 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1684 yield self
1686 for testsuite in self._testsuites.values():
1687 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1689 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme: 1689 ↛ 1690line 1689 didn't jump to line 1690 because the condition on line 1689 was never true
1690 yield self
1692 def __str__(self) -> str:
1693 return (
1694 f"<TestsuiteSummary {self._name}: {self._status.name} -"
1695 # f" assert/pass/fail:{self._assertionCount}/{self._passedAssertionCount}/{self._failedAssertionCount} -"
1696 f" warn/error/fatal:{self._warningCount}/{self._errorCount}/{self._fatalCount}>"
1697 )
1700@export
1701class Document(metaclass=ExtendedType, mixin=True):
1702 """A mixin-class representing a unit test summary document (file)."""
1704 _path: Path
1706 _analysisDuration: float #: TODO: replace by Timer; should be timedelta?
1707 _modelConversion: float #: TODO: replace by Timer; should be timedelta?
1709 def __init__(self, reportFile: Path, analyzeAndConvert: bool = False) -> None:
1710 self._path = reportFile
1712 self._analysisDuration = -1.0
1713 self._modelConversion = -1.0
1715 if analyzeAndConvert:
1716 self.Analyze()
1717 self.Convert()
1719 @readonly
1720 def Path(self) -> Path:
1721 """
1722 Read-only property to access the path to the file of this document.
1724 :returns: The document's path to the file.
1725 """
1726 return self._path
1728 @readonly
1729 def AnalysisDuration(self) -> timedelta:
1730 """
1731 Read-only property returning analysis duration.
1733 .. note::
1735 This includes usually the duration to validate and parse the file format, but it excludes the time to convert the
1736 content to the test entity hierarchy.
1738 :returns: Duration to analyze the document.
1739 """
1740 return timedelta(seconds=self._analysisDuration)
1742 @readonly
1743 def ModelConversionDuration(self) -> timedelta:
1744 """
1745 Read-only property returning conversion duration.
1747 .. note::
1749 This includes usually the duration to convert the document's content to the test entity hierarchy. It might also
1750 include the duration to (re-)aggregate all states and statistics in the hierarchy.
1752 :returns: Duration to convert the document.
1753 """
1754 return timedelta(seconds=self._modelConversion)
1756 @abstractmethod
1757 def Analyze(self) -> None:
1758 """Analyze and validate the document's content."""
1760 # @abstractmethod
1761 # def Write(self, path: Nullable[Path] = None, overwrite: bool = False):
1762 # pass
1764 @abstractmethod
1765 def Convert(self):
1766 """Convert the document's content to an instance of the test entity hierarchy."""
1769@export
1770class Merged(metaclass=ExtendedType, mixin=True):
1771 """A mixin-class representing a merged test entity."""
1773 _mergedCount: int
1775 def __init__(self, mergedCount: int = 1) -> None:
1776 self._mergedCount = mergedCount
1778 @readonly
1779 def MergedCount(self) -> int:
1780 return self._mergedCount
1783@export
1784class Combined(metaclass=ExtendedType, mixin=True):
1785 _combinedCount: int
1787 def __init__(self, combinedCound: int = 1) -> None:
1788 self._combinedCount = combinedCound
1790 @readonly
1791 def CombinedCount(self) -> int:
1792 return self._combinedCount
1795@export
1796class MergedTestcase(Testcase, Merged):
1797 _mergedTestcases: List[Testcase]
1799 def __init__(
1800 self,
1801 testcase: Testcase,
1802 parent: Nullable["Testsuite"] = None
1803 ) -> None:
1804 if testcase is None: 1804 ↛ 1805line 1804 didn't jump to line 1805 because the condition on line 1804 was never true
1805 raise ValueError(f"Parameter 'testcase' is None.")
1807 super().__init__(
1808 testcase._name,
1809 testcase._startTime,
1810 testcase._setupDuration, testcase._testDuration, testcase._teardownDuration, testcase._totalDuration,
1811 TestcaseStatus.Unknown,
1812 testcase._assertionCount, testcase._failedAssertionCount, testcase._passedAssertionCount,
1813 testcase._warningCount, testcase._errorCount, testcase._fatalCount,
1814 testcase._expectedWarningCount, testcase._expectedErrorCount, testcase._expectedFatalCount,
1815 parent
1816 )
1817 Merged.__init__(self)
1819 self._mergedTestcases = [testcase]
1821 @readonly
1822 def Status(self) -> TestcaseStatus:
1823 if self._status is TestcaseStatus.Unknown: 1823 ↛ 1830line 1823 didn't jump to line 1830 because the condition on line 1823 was always true
1824 status = self._mergedTestcases[0]._status
1825 for mtc in self._mergedTestcases[1:]:
1826 status @= mtc._status
1828 self._status = status
1830 return self._status
1832 @readonly
1833 def SummedAssertionCount(self) -> int:
1834 return sum(tc._assertionCount for tc in self._mergedTestcases)
1836 @readonly
1837 def SummedPassedAssertionCount(self) -> int:
1838 return sum(tc._passedAssertionCount for tc in self._mergedTestcases)
1840 @readonly
1841 def SummedFailedAssertionCount(self) -> int:
1842 return sum(tc._failedAssertionCount for tc in self._mergedTestcases)
1844 def Aggregate(self, strict: bool = True) -> TestcaseAggregateReturnType:
1845 firstMTC = self._mergedTestcases[0]
1847 status = firstMTC._status
1848 warningCount = firstMTC._warningCount
1849 errorCount = firstMTC._errorCount
1850 fatalCount = firstMTC._fatalCount
1851 totalDuration = firstMTC._totalDuration
1853 for mtc in self._mergedTestcases[1:]:
1854 status @= mtc._status
1855 warningCount += mtc._warningCount
1856 errorCount += mtc._errorCount
1857 fatalCount += mtc._fatalCount
1859 self._status = status
1861 return warningCount, errorCount, fatalCount, self._expectedWarningCount, self._expectedErrorCount, self._expectedFatalCount, totalDuration
1863 def Merge(self, tc: Testcase) -> None:
1864 self._mergedCount += 1
1866 self._mergedTestcases.append(tc)
1868 self._warningCount += tc._warningCount
1869 self._errorCount += tc._errorCount
1870 self._fatalCount += tc._fatalCount
1872 def ToTestcase(self) -> Testcase:
1873 return Testcase(
1874 self._name,
1875 self._startTime,
1876 self._setupDuration,
1877 self._testDuration,
1878 self._teardownDuration,
1879 self._totalDuration,
1880 self._status,
1881 self._assertionCount,
1882 self._failedAssertionCount,
1883 self._passedAssertionCount,
1884 self._warningCount,
1885 self._errorCount,
1886 self._fatalCount
1887 )
1890@export
1891class MergedTestsuite(Testsuite, Merged):
1892 def __init__(
1893 self,
1894 testsuite: Testsuite,
1895 addTestsuites: bool = False,
1896 addTestcases: bool = False,
1897 parent: Nullable["Testsuite"] = None
1898 ) -> None:
1899 if testsuite is None: 1899 ↛ 1900line 1899 didn't jump to line 1900 because the condition on line 1899 was never true
1900 raise ValueError(f"Parameter 'testsuite' is None.")
1902 super().__init__(
1903 testsuite._name,
1904 testsuite._kind,
1905 testsuite._startTime,
1906 testsuite._setupDuration, testsuite._testDuration, testsuite._teardownDuration, testsuite._totalDuration,
1907 TestsuiteStatus.Unknown,
1908 testsuite._warningCount, testsuite._errorCount, testsuite._fatalCount,
1909 parent
1910 )
1911 Merged.__init__(self)
1913 if addTestsuites: 1913 ↛ 1918line 1913 didn't jump to line 1918 because the condition on line 1913 was always true
1914 for ts in testsuite._testsuites.values():
1915 mergedTestsuite = MergedTestsuite(ts, addTestsuites, addTestcases)
1916 self.AddTestsuite(mergedTestsuite)
1918 if addTestcases: 1918 ↛ exitline 1918 didn't return from function '__init__' because the condition on line 1918 was always true
1919 for tc in testsuite._testcases.values():
1920 mergedTestcase = MergedTestcase(tc)
1921 self.AddTestcase(mergedTestcase)
1923 def Merge(self, testsuite: Testsuite) -> None:
1924 self._mergedCount += 1
1926 for ts in testsuite._testsuites.values():
1927 if ts._name in self._testsuites: 1927 ↛ 1930line 1927 didn't jump to line 1930 because the condition on line 1927 was always true
1928 self._testsuites[ts._name].Merge(ts)
1929 else:
1930 mergedTestsuite = MergedTestsuite(ts, addTestsuites=True, addTestcases=True)
1931 self.AddTestsuite(mergedTestsuite)
1933 for tc in testsuite._testcases.values():
1934 if tc._name in self._testcases: 1934 ↛ 1937line 1934 didn't jump to line 1937 because the condition on line 1934 was always true
1935 self._testcases[tc._name].Merge(tc)
1936 else:
1937 mergedTestcase = MergedTestcase(tc)
1938 self.AddTestcase(mergedTestcase)
1940 def ToTestsuite(self) -> Testsuite:
1941 testsuite = Testsuite(
1942 self._name,
1943 self._kind,
1944 self._startTime,
1945 self._setupDuration,
1946 self._testDuration,
1947 self._teardownDuration,
1948 self._totalDuration,
1949 self._status,
1950 self._warningCount,
1951 self._errorCount,
1952 self._fatalCount,
1953 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values()),
1954 testcases=(tc.ToTestcase() for tc in self._testcases.values())
1955 )
1957 testsuite._tests = self._tests
1958 testsuite._excluded = self._excluded
1959 testsuite._inconsistent = self._inconsistent
1960 testsuite._skipped = self._skipped
1961 testsuite._errored = self._errored
1962 testsuite._weak = self._weak
1963 testsuite._failed = self._failed
1964 testsuite._passed = self._passed
1966 return testsuite
1969@export
1970class MergedTestsuiteSummary(TestsuiteSummary, Merged):
1971 _mergedFiles: Dict[Path, TestsuiteSummary]
1973 def __init__(self, name: str) -> None:
1974 super().__init__(name)
1975 Merged.__init__(self, mergedCount=0)
1977 self._mergedFiles = {}
1979 def Merge(self, testsuiteSummary: TestsuiteSummary) -> None:
1980 # if summary.File in self._mergedFiles:
1981 # raise
1983 # FIXME: a summary is not necessarily a file
1984 self._mergedCount += 1
1985 self._mergedFiles[testsuiteSummary._name] = testsuiteSummary
1987 for testsuite in testsuiteSummary._testsuites.values():
1988 if testsuite._name in self._testsuites:
1989 self._testsuites[testsuite._name].Merge(testsuite)
1990 else:
1991 mergedTestsuite = MergedTestsuite(testsuite, addTestsuites=True, addTestcases=True)
1992 self.AddTestsuite(mergedTestsuite)
1994 def ToTestsuiteSummary(self) -> TestsuiteSummary:
1995 testsuiteSummary = TestsuiteSummary(
1996 self._name,
1997 self._startTime,
1998 self._setupDuration,
1999 self._testDuration,
2000 self._teardownDuration,
2001 self._totalDuration,
2002 self._status,
2003 self._warningCount,
2004 self._errorCount,
2005 self._fatalCount,
2006 testsuites=(ts.ToTestsuite() for ts in self._testsuites.values())
2007 )
2009 testsuiteSummary._tests = self._tests
2010 testsuiteSummary._excluded = self._excluded
2011 testsuiteSummary._inconsistent = self._inconsistent
2012 testsuiteSummary._skipped = self._skipped
2013 testsuiteSummary._errored = self._errored
2014 testsuiteSummary._weak = self._weak
2015 testsuiteSummary._failed = self._failed
2016 testsuiteSummary._passed = self._passed
2018 return testsuiteSummary