diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 35a2c6a..44b952f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -18,5 +18,18 @@ jobs: - name: Build source code run: ./gradlew build -x test - - name: Run tests with jacoco - run: ./gradlew test + - name: Run tests + run: ./gradlew test >./test-res-out.log 2>./test-res-err.log + continue-on-error: true + + - name: Display test results + run: python3 ./scripts/test-result-printer.py --dir ./lib/build/test-results/test --all-failures + + - name: Run jacoco coverage report + run: ./gradlew jacocoTestReport >./test-res-out.log 2>./test-res-err.log + + - name: Display info about coverage + run: python3 ./scripts/csv-reports-printer.py --input ./lib/build/reports/jacoco/info.csv --lib lib + + - name: Clear tmpfiles of runnig tests + run: rm ./test-res-out.log && rm ./test-res-err.log diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 48a49ca..62c75da 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -4,9 +4,6 @@ plugins { // Code coverage plugin jacoco - - // Output project coverage - id("org.barfuin.gradle.jacocolog") version "3.1.0" } repositories { @@ -17,6 +14,8 @@ repositories { dependencies { // This dependency is exported to consumers, that is to say found on their compile classpath. api(libs.commons.math3) + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") // This dependency is used internally, and not exposed to consumers on their own compile classpath. @@ -30,32 +29,23 @@ java { } } -testing { - suites { - // Configure the built-in test suite - val test by getting(JvmTestSuite::class) { - // Use Kotlin Test framework - useKotlinTest("1.9.20") - } - } -} - tasks.named("test") { useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) -} - -tasks.withType { testLogging { - events("PASSED", "SKIPPED", "FAILED") + showCauses = false + showStackTraces = false + showExceptions = false } } tasks.named("jacocoTestReport") { - dependsOn(tasks.test) + val sep = File.separator + val jacocoReportsDirName = "reports${sep}jacoco" reports { - csv.required = false + csv.required = true xml.required = false - html.outputLocation = layout.buildDirectory.dir("jacocoHtml") + html.required = true + csv.outputLocation = layout.buildDirectory.file("${jacocoReportsDirName}${sep}info.csv") + html.outputLocation = layout.buildDirectory.dir("${jacocoReportsDirName}${sep}html") } -} +} \ No newline at end of file diff --git a/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt b/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt index 6cf81ca..12aa1ac 100644 --- a/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt +++ b/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt @@ -41,7 +41,7 @@ public open class RBTree, V>: AbstractBSTree?, V?> var resultCompare: Int = key.compareTo(node.key) - var nodeCurrent: RBTreeNode = node + var nodeCurrent = node if (resultCompare < 0) { if (!isRedColor(nodeCurrent.leftChild) && !isRedLeftChild(nodeCurrent.leftChild)) nodeCurrent = moveRedLeft(nodeCurrent) @@ -105,13 +105,13 @@ public open class RBTree, V>: AbstractBSTree): RBTreeNode { - val rightChild: RBTreeNode = node.rightChild ?: return node - node.rightChild = rightChild.leftChild - rightChild.leftChild = node + val nodeSwapped: RBTreeNode = node.rightChild ?: return node + node.rightChild = nodeSwapped.leftChild + nodeSwapped.leftChild = node - rightChild.isRed = node.isRed + nodeSwapped.isRed = node.isRed node.isRed = true - return rightChild + return nodeSwapped } /** @@ -122,13 +122,13 @@ public open class RBTree, V>: AbstractBSTree): RBTreeNode { - val leftChild: RBTreeNode = node.leftChild ?: return node - node.leftChild = leftChild.rightChild - leftChild.rightChild = node + val nodeSwapped: RBTreeNode = node.leftChild ?: return node + node.leftChild = nodeSwapped.rightChild + nodeSwapped.rightChild = node - leftChild.isRed = node.isRed + nodeSwapped.isRed = node.isRed node.isRed = true - return leftChild + return nodeSwapped } /** @@ -151,7 +151,7 @@ public open class RBTree, V>: AbstractBSTree): RBTreeNode { if (node.rightChild == null) return node - var nodeCurrent: RBTreeNode = node + var nodeCurrent = node flipColors(nodeCurrent) if (isRedLeftChild(nodeCurrent.leftChild)) { @@ -170,13 +170,13 @@ public open class RBTree, V>: AbstractBSTree): RBTreeNode { if (node.leftChild == null) return node - var nodeCurrent: RBTreeNode = node + var nodeCurrent = node flipColors(nodeCurrent) if (isRedLeftChild(nodeCurrent.rightChild)) { nodeCurrent.rightChild = notNullNodeAction( node.rightChild, null - ) {rightChild -> rotateRight(rightChild)} + ) { rightChild -> rotateRight(rightChild) } nodeCurrent = rotateLeft(nodeCurrent) flipColors(nodeCurrent) } @@ -193,7 +193,7 @@ public open class RBTree, V>: AbstractBSTree = node + var nodeCurrent = node if (!isRedColor(leftChild) && !isRedLeftChild(leftChild)) nodeCurrent = moveRedLeft(nodeCurrent) diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt index 124e62e..5296d7c 100644 --- a/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt @@ -26,42 +26,42 @@ class AVLTreeTest { Assertions.assertEquals(0, tree.size) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeCreationCases") @DisplayName("node creation") public fun testNodeCreation(key: Int, value: Int) { tree.assertNodeCreation(key, value) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testBalanceTreeCases") @DisplayName("check balance tree") public fun testBalanceTree(expected: AVLTreeNode, node: AVLTreeNode) { tree.assertBalanceTree(expected, node) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("checkBalanceFactor") @DisplayName("balance factor") public fun checkBalanceFactor(expected: Int, node: AVLTreeNode?) { tree.assertBalanceFactor(expected, node) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testBalanceCases") @DisplayName("balance case") public fun testBalanceCase(expected: AVLTreeNode, node: AVLTreeNode) { tree.assertBalance(expected, node) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeRotateLeftCases") @DisplayName("node rotate left case") public fun testNodeRotateLeftCases(expected: AVLTreeNode, node: AVLTreeNode) { tree.assertNodeLeftRotation(expected, node) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeRotateRightCases") @DisplayName("node rotate right case") public fun testNodeRotateRightCase(expected: AVLTreeNode, node: AVLTreeNode) { diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt index 13da642..226b109 100644 --- a/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt @@ -12,6 +12,7 @@ import tree_tripper.iterators.IterationOrders import java.time.Duration import kotlin.random.Random import kotlin.test.Test +import kotlin.test.assertEquals public class BSTreeTest { @@ -59,7 +60,7 @@ public class BSTreeTest { Assertions.assertEquals(tree.getRoot(), Pair(1, 0), "Incorrect change root") } - @Test + @Test() @DisplayName("insert children root") public fun testInsertChildrenRoot() { tree.insert(2, -2) @@ -69,7 +70,7 @@ public class BSTreeTest { Assertions.assertEquals(tree.size, 3, "Incorrect resizing tree size.") } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("getSizeAndTimeArguments") @DisplayName("insert with size and time") public fun testInsertWithSizeAndTime(size: Int, seconds: Long) { @@ -115,7 +116,7 @@ public class BSTreeTest { Assertions.assertEquals(tree.search(0), null, "Incorrect search a non-existent child root.") } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("getSizeAndTimeArguments") @DisplayName("search with size and time") public fun testSearchWithSizeAndTime(size: Int, seconds: Long) { @@ -285,7 +286,7 @@ public class BSTreeTest { Assertions.assertEquals(tree.search(3), -3, "Incorrect remove a root and lose the right child.") } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("getSizeAndTimeArguments") @DisplayName("remove with size and time") public fun testRemoveWithSizeAndTime(size: Int, seconds: Long) { diff --git a/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt b/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt index 0cbf597..3ac0388 100644 --- a/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt +++ b/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt @@ -11,7 +11,7 @@ import tree_tripper.nodes.binary_nodes.BSTreeNode class BinarySearchTreeIteratorTest { lateinit var iterator: BinarySearchTreeIterator> - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testIteratorCases") @DisplayName("test iterator at width order") public fun testWidthOrderIterator(expected: List, root: BSTreeNode) { @@ -24,7 +24,7 @@ class BinarySearchTreeIteratorTest { } } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testGetALotOfElementsCases") @DisplayName("try get more elements than iterator has") public fun testGetALotOfElements(order: IterationOrders) { @@ -33,7 +33,7 @@ class BinarySearchTreeIteratorTest { Assertions.assertThrows(NoSuchElementException::class.java) { iterator.next() } } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testIteratorCases") @DisplayName("test iterator at increase order") public fun testIncreasingOrderIterator(expected: List, root: BSTreeNode) { @@ -47,7 +47,7 @@ class BinarySearchTreeIteratorTest { } } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testIteratorCases") @DisplayName("test iterator at decrease order") public fun testDecreasingOrderIterator(expected: List, root: BSTreeNode) { diff --git a/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt index cc2fa1a..c21815d 100644 --- a/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt +++ b/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt @@ -10,7 +10,7 @@ import tree_tripper.nodes.binary_nodes.BSTreeNode public class UtilsTest { - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeUpdateCases") @DisplayName("util of node update") public fun testNodeUpdate(expected: Boolean, node: BSTreeNode?) { @@ -19,7 +19,7 @@ public class UtilsTest { Assertions.assertEquals(expected, isActivateAction) } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeActionCases") @DisplayName("util of action on node") public fun testNodeAction(expected: Boolean, node: BSTreeNode?) { diff --git a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt index cad70a1..ca1a2b2 100644 --- a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt +++ b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt @@ -17,7 +17,7 @@ class AVLTreeNodeTest { Assertions.assertEquals(1, node.height) {"The height is not 1 by standard initialize."} } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testUpdateHeight") @DisplayName("update height") public fun testUpdateHeight(expected: Int, node: AVLTreeNode) { diff --git a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt index 5a71782..ac95991 100644 --- a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt +++ b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt @@ -10,7 +10,7 @@ import org.junit.jupiter.params.provider.MethodSource public class RBTreeNodeTest { - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeSimpleInitializeCases") @DisplayName("node simple initialization") public fun testNodeSimpleInitialize(key: Int, value: Int?) { @@ -22,7 +22,7 @@ public class RBTreeNodeTest { Assertions.assertNull(node.rightChild) { "Right child of node is not null." } } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeColorTypeInitializeCases") @DisplayName("node initialization with color") public fun testNodeColorTypeInitialize(isRed: Boolean) { @@ -32,7 +32,7 @@ public class RBTreeNodeTest { Assertions.assertNull(node.rightChild) { "Right child of node is not null." } } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testNodeFullInitializeCases") @DisplayName("node initialization with color and children") public fun testNodeFullInitialize(leftChild: RBTreeNode?, rightChild: RBTreeNode?) { @@ -41,7 +41,7 @@ public class RBTreeNodeTest { assertBinaryNodeDeepEquals(rightChild, node.rightChild) { n1, n2 -> n1.isRed == n2.isRed } } - @ParameterizedTest + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") @MethodSource("testToStringSimpleViewCases") @DisplayName("to string simple view") public fun testToStringSimpleView(expected: String, node: RBTreeNode) { diff --git a/scripts/csv-reports-printer.py b/scripts/csv-reports-printer.py new file mode 100644 index 0000000..4ee0bbe --- /dev/null +++ b/scripts/csv-reports-printer.py @@ -0,0 +1,198 @@ +import argparse +import csv +import sys +import typing +from text_colorize import ANSIColors, TextStyle, colorize + + +COLUMNS_TYPES = [ + '_MISSED', + '_COVERED', +] + +CSV_COLUMNS = [ + 'PACKAGES', + 'CLASS', + 'BRANCH_MISSED', + 'BRANCH_COVERED', + 'LINE_MISSED', + 'LINE_COVERED', + 'METHOD_MISSED', + 'METHOD_COVERED', +] +DISPLAY_COLUMNS = [ + 'PACKAGES', + 'CLASS', + 'BRANCH', + 'LINE', + 'METHOD', +] +DEFAULT_LABEL_SIZE = 8 + + +def create_row_info() -> dict: + return { + key: 0 for key in CSV_COLUMNS + } + + +def is_valid_lib(group: str, lib_name: str) -> bool: + if len(lib_name) == 0: + return True + return group == lib_name + + +def parse_args(args: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="CSV Jacoco Test Reports Printer", + description="Program read csv file with jacoco report and print it at terminal at stdin", + ) + + parser.add_argument( + "-i", "--input", required=True, + help="setup path to CSV file with jacoco report information", + metavar="" + ) + parser.add_argument( + "-l", "--lib", + help="setup library name to remove from package path", + default="", + metavar="" + ) + parser.add_argument( + "-p", "--package-print", + help="setup flag to 'ON' to print packages of files at report (default 'OFF')", + action='store_true', + default=False + ) + + return parser.parse_args(args) + + +def read_csv(namespace: argparse.Namespace) -> typing.Optional[dict]: + table = [] + max_packages_name_length = 20 + max_classes_name_length = 20 + + with open(getattr(namespace, "input"), 'r') as file: + reader = csv.reader(file) + for row in reader: + if len(row) == 0: + break + if (row[0] == "GROUP") or not is_valid_lib(row[0], getattr(namespace, "lib", "")): + continue + + row_info = create_row_info() + is_skipped = False + for key in row_info.keys(): + if key not in CSV_COLUMNS: + row_info.pop(key) + + index = CSV_COLUMNS.index(key) + 1 + row_info[key] = row[index] + + if key == "PACKAGES": + max_packages_name_length = max(max_packages_name_length, len(row_info[key])) + elif key == "CLASS": + if '(' in row_info[key] or ')' in row_info[key] or ' ' in row_info[key]: + is_skipped = True + break + elif '.' in row_info[key]: + row_info[key] = row_info[key].split('.')[-1] + + max_classes_name_length = max(max_classes_name_length, len(row_info[key])) + + if not is_skipped: + table.append(row_info) + return { + "table": table, + "max_packages_name_length": max_packages_name_length, + "max_classes_name_length": max_classes_name_length + } + + +def create_label(text: str, lbl_size: int, color: ANSIColors = ANSIColors.BLACK): + if len(text) >= lbl_size: + text = text[:lbl_size] + + if len(text) % 2 != 0: + text = ' ' + text + + color_text = colorize( + f"{{:^{lbl_size}}}".format(text), + color, + TextStyle.BOLD + ) + return f'| {color_text} |' + + +def colorize_percent_label(percent: int) -> str: + color = ANSIColors.RED + if 50 <= percent < 75: + color = ANSIColors.YELLOW + elif 75 <= percent <= 100: + color = ANSIColors.GREEN + + return create_label(f"{percent}%", DEFAULT_LABEL_SIZE, color) + + +def display_columns(max_packages_name_length: int, max_classes_name_length: int) -> int: + global DISPLAY_COLUMNS, DEFAULT_LABEL_SIZE + for column in DISPLAY_COLUMNS: + lbl_size = DEFAULT_LABEL_SIZE + if column == "CLASS": + lbl_size = max_classes_name_length + elif column == "PACKAGES": + lbl_size = max_packages_name_length + + lbl = create_label(column, lbl_size, ANSIColors.PURPLE) + print(lbl, end="") + print() + + +def display_csv_data(namespace: argparse.Namespace, csv_data_dict: dict) -> None: + global DISPLAY_COLUMNS, COLUMNS_TYPES + if not getattr(namespace, "package_print", False): + DISPLAY_COLUMNS.remove("PACKAGES") + + if getattr(namespace, 'lib'): + print(f"Jacoco Covered Report Info for module named '{getattr(namespace, 'lib')}':") + + max_packages_name_length = csv_data_dict.get("max_packages_name_length", 20) + max_classes_name_length = csv_data_dict.get("max_classes_name_length", 20) + table: list[dict] = csv_data_dict.get("table", []) + display_columns(max_packages_name_length, max_classes_name_length) + + for row in table: + for column in DISPLAY_COLUMNS: + lbl = "" + if column in ["PACKAGES", "CLASS"]: + lbl = create_label( + row[column], + max_packages_name_length if column == "PACKAGES" else max_classes_name_length, + ANSIColors.YELLOW + ) + else: + vals = [int(row[column + _type]) for _type in COLUMNS_TYPES] + percent = int(round((vals[1] - vals[0]) / vals[1], 2) * 100) if vals[1] != 0 else 100 + lbl = colorize_percent_label( + percent, + ) + + print(lbl, end="") + print() + + +if __name__ == "__main__": + ns = parse_args(sys.argv[1:]) + + try: + csv_data = read_csv(ns) + except Exception as e: + print( + f"Can't read csv file: '{getattr(ns, 'input')}', get exception: '{e}'", + file=sys.stderr + ) + sys.exit(-1) + + display_csv_data(ns, csv_data) diff --git a/scripts/test-result-printer.py b/scripts/test-result-printer.py new file mode 100644 index 0000000..5f9e987 --- /dev/null +++ b/scripts/test-result-printer.py @@ -0,0 +1,185 @@ +import argparse +import sys, os +import xml.etree.ElementTree as ET +from text_colorize import ANSIColors, TextStyle, colorize + + +class TestCase: + + def __init__(self, name: str, is_passed: bool) -> None: + self.name = str(name) + self.is_passed = bool(is_passed) + + def toString(self, indent: int = 0): + return "\t" * indent + f"{self.name} -> {self.result_type()}" + + def result_type(self) -> str: + return colorize( + 'PASSED' if self.is_passed else 'FAILURE', + ANSIColors.GREEN if self.is_passed else ANSIColors.RED, + TextStyle.ITALIC + ) + + def __bool__(self): + return self.is_passed + + +class ParametrisedTestCase(TestCase): + + def __init__(self, name: str, cases: list[TestCase]) -> None: + super().__init__(name, all(cases)) + self.cases = cases + + def add_case(self, case: TestCase): + self.is_passed = self.is_passed and case.is_passed + self.cases.append(case) + + def toString(self, indent: int = 0, also_failed: bool = False): + inline_cases = [] + for case in self.cases: + if (also_failed and case.is_passed): + continue + + inline_cases.append(case.toString(indent + 1)) + + inline_cases = '\n'.join(inline_cases).rstrip() + return super().toString(indent) + f"\n{inline_cases}" + + +def parse_args(args: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="Test Result Printer", + description="Program read xml files with tests results and print it at terminal at stdin", + ) + parser.add_argument( + "-d", "--dir", required=True, + help="setup path to directory with xml files with tests information", + metavar="" + ) + parser.add_argument( + "-a", "--all", + action="store_true", + help="setup mode of diplay type to print all test information to ON (default OFF)", + ) + parser.add_argument( + "-f", "--all-failures", + action="store_true", + help="setup mode of diplay type to print also test information which failed to ON (default OFF)", + ) + return parser.parse_args(args) + + +def parse_test_result(test_path: str) -> tuple[ET.Element, dict[str, TestCase]]: + tree_root = ET.parse(test_path).getroot() + cases = dict() + for child in tree_root: + if child.tag != "testcase": + continue + + name = child.get("name", "uncknown test cases") + is_passed = child.find("failure") is None + if '[' in name: + primary_name = name.split('[')[0] + args = '[' + "[".join(name.split('[')[1:]) + + case: ParametrisedTestCase = cases.get(primary_name, ParametrisedTestCase(primary_name, [])) + case.add_case(TestCase(args, is_passed)) + cases[primary_name] = case + else: + cases[name] = TestCase(name, is_passed) + + return (tree_root, cases,) + + +def display_all_test_result(tree_root: ET.Element, cases: dict[str, TestCase]): + print( + "Tests of", + tree_root.attrib.get("name", "UncnownTestSuite").split('.')[-1].replace("Test", ":"), + sep=" " + ) + for name in sorted(cases.keys()): + print(cases[name].toString(indent=1)) + + passed_test_count = int(tree_root.attrib.get("tests", 0)) - int(tree_root.attrib.get("failures", 0)) + print( + colorize(f"Passed: {passed_test_count}", ANSIColors.GREEN, TextStyle.BOLD), + colorize(f"Failures: {tree_root.attrib.get('failures', 0)}", ANSIColors.RED, TextStyle.BOLD), + f"Time: {tree_root.attrib.get('time', 0.0)}", + sep=" ", + end=os.linesep * 2 + ) + + +def display_failures_test_result(tree_root: ET.Element, cases: dict[str, TestCase]): + failed_tests = [] + for name in sorted(cases.keys()): + if not cases[name].is_passed: + failed_tests.append(cases[name]) + + if len(failed_tests) == 0: + return + + print( + "Failed tests of", + tree_root.attrib.get("name", "UncnownTestSuite").split('.')[-1].replace("Test", ":"), + sep=" " + ) + for case in failed_tests: + if isinstance(case, ParametrisedTestCase): + print(case.toString(indent=1, also_failed=True)) + elif isinstance(case, TestCase): + print(case.toString(indent=1)) + + passed_test_count = int(tree_root.attrib.get("tests", 0)) - int(tree_root.attrib.get("failures", 0)) + print( + colorize(f"Passed: {passed_test_count}", ANSIColors.GREEN, TextStyle.BOLD), + colorize(f"Failures: {tree_root.attrib.get('failures', 0)}", ANSIColors.RED, TextStyle.BOLD), + f"Time: {tree_root.attrib.get('time', 0.0)}", + sep=" ", + end=os.linesep * 2 + ) + + +if __name__ == "__main__": + ns = parse_args(sys.argv[1:]) + + tests_result_dir = getattr(ns, "dir") + childs = os.listdir(tests_result_dir) + tests_results: list[tuple[ET.Element, dict[str, TestCase]]] = [] + for child in sorted(childs): + child_path = os.path.join(tests_result_dir, child) + if not os.path.isfile(child_path): + continue + + if not (child.startswith("TEST") and child.endswith(".xml")): + continue + + try: + tests_results.append(parse_test_result(child_path)) + except Exception as e: + print(f"Can't display ttest information at file '{child}': {e}", file=sys.stderr) + + tests_count = 0 + tests_failed_count = 0 + time_of_all_tests = 0 + for test_result in tests_results: + tree_root: ET.Element = test_result[0] + tests_count += int(tree_root.attrib.get("tests", 0)) + tests_failed_count += int(tree_root.attrib.get("failures", 0)) + time_of_all_tests += float(tree_root.attrib.get('time', 0.0)) + + + print( + colorize(f"Count of tests: {tests_count}", ANSIColors.YELLOW, TextStyle.BOLD), + colorize(f"Count of passed tests: {tests_count - tests_failed_count}", ANSIColors.GREEN, TextStyle.BOLD), + colorize(f"Count of failured tests: {tests_failed_count}", ANSIColors.RED, TextStyle.BOLD), + colorize(f"Time: {time_of_all_tests}", ANSIColors.BLUE, TextStyle.BOLD), + sep=os.linesep, + end=os.linesep * 2 + ) + + for test_result in tests_results: + if getattr(ns, "all", False): + display_all_test_result(test_result[0], test_result[1]) + elif getattr(ns, "all_failures", False): + display_failures_test_result(test_result[0], test_result[1]) diff --git a/scripts/text_colorize.py b/scripts/text_colorize.py new file mode 100644 index 0000000..ce38225 --- /dev/null +++ b/scripts/text_colorize.py @@ -0,0 +1,20 @@ +import enum + +class ANSIColors(enum.IntEnum): + PURPLE = 35 + BLUE = 34 + GREEN = 32 + YELLOW = 33 + RED = 31 + BLACK = 30 + WHITE = 37 + + +class TextStyle(enum.IntEnum): + SIMPLE = 0 + BOLD = 1 + ITALIC = 3 + + +def colorize(text: str, color: ANSIColors, style: TextStyle = TextStyle.SIMPLE) -> str: + return f"\033[{style};{color}m{text}\033[0m"