| #!/usr/bin/env python3 |
| # Copyright 2021 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Graph analysis functions for the testing framework. |
| """ |
| |
| import logging |
| from typing import List, Optional, Tuple |
| |
| from models import ActionCoverage |
| from models import ActionType |
| from models import Action |
| from models import ActionNode |
| from models import CoverageTest |
| from models import TestPlatform |
| |
| |
| def build_action_node_graph(root_node: ActionNode, |
| tests: List[CoverageTest]) -> None: |
| """ |
| Builds a graph of ActionNodes on the given `root_node`. The graph is |
| generated by iterating the `tests`, starting at the `root_node` for each |
| test, and adding ActionNodes to the graph for each state-change action |
| (following the graph for every state-change action). State-check actions are |
| added to the previous state-change action ActionNode. |
| """ |
| assert root_node is not None |
| assert isinstance(root_node, ActionNode) |
| assert isinstance(tests, list) |
| for test in tests: |
| assert isinstance(test, CoverageTest) |
| parent = root_node |
| for action in test.actions: |
| assert isinstance(action, Action) |
| assert action.type is not ActionType.PARAMETERIZED |
| if action.is_state_check(): |
| parent.add_state_check_action(action) |
| else: |
| node = None |
| if action.name in parent.children: |
| node = parent.children[action.name] |
| else: |
| node = ActionNode(action) |
| parent.add_child(node) |
| parent = node |
| |
| |
| def generage_graphviz_dot_file(root_node: ActionNode, |
| platform: Optional[TestPlatform]) -> str: |
| def get_all_nodes_and_assign_graph_ids(root: ActionNode |
| ) -> List[ActionNode]: |
| current_graph_id = 0 |
| |
| def get_all_nodes_helper(node: ActionNode, nodes: List[ActionNode]): |
| nonlocal current_graph_id |
| if node.graph_id is not None: |
| return |
| node.graph_id = current_graph_id |
| current_graph_id += 1 |
| nodes.append(node) |
| if not node.children: |
| return |
| for child in node.children.values(): |
| get_all_nodes_helper(child, nodes) |
| |
| # Skip the root node, as it is only there for the algorithm to work. |
| all_nodes = [] |
| for child in root.children.values(): |
| get_all_nodes_helper(child, all_nodes) |
| return all_nodes |
| |
| def print_graph(node: ActionNode) -> List[str]: |
| assert node.graph_id is not None, node.action.name |
| if not node.children: |
| return [] |
| lines = [] |
| for child in node.children.values(): |
| assert child.graph_id is not None, child.action.name |
| edge_str = str(node.graph_id) + " -> " + str(child.graph_id) |
| lines.append(edge_str) |
| lines.extend(print_graph(child)) |
| return lines |
| |
| lines = [] |
| lines.append("strict digraph {") |
| nodes = get_all_nodes_and_assign_graph_ids(root_node) |
| for node in nodes: |
| color_str = ("seagreen" if platform is None |
| or platform in node.action.full_coverage_platforms else |
| "sandybrown") |
| label_str = node.get_graphviz_label() |
| lines.append(f"{node.graph_id}[label={label_str} color={color_str}]") |
| # Skip the root node, as it is only there for the algorithm to work. |
| for child in root_node.children.values(): |
| lines.extend(print_graph(child)) |
| lines.append("}") |
| return "\n".join(lines) |
| |
| |
| # Removes any nodes and actions from the graph that are not supported by the |
| # given platform. |
| def trim_graph_to_platform_actions(root_node: ActionNode, |
| platform: TestPlatform) -> None: |
| """ |
| Removes any nodes and actions from the graph that are not supported by the |
| given platform. |
| """ |
| new_children = {} |
| for child in root_node.children.values(): |
| if child.action.supported_for_platform(platform): |
| new_children[child.action.name] = child |
| root_node.children = new_children |
| new_state_check_actions = {} |
| for action in root_node.state_check_actions.values(): |
| if action.supported_for_platform(platform): |
| new_state_check_actions[action.name] = action |
| root_node.state_check_actions = new_state_check_actions |
| for child in root_node.children.values(): |
| trim_graph_to_platform_actions(child, platform) |
| |
| |
| def generate_framework_tests(root_node: ActionNode, |
| platform: TestPlatform) -> List[CoverageTest]: |
| assert isinstance(root_node, ActionNode) |
| |
| def GetAllPaths(node: ActionNode) -> List[List[ActionNode]]: |
| assert node is not None |
| assert isinstance(node, ActionNode) |
| paths = [] |
| for child in node.children.values(): |
| for path in GetAllPaths(child): |
| assert path is not None |
| assert isinstance(path, list) |
| assert bool(path) |
| paths.append([node] + path) |
| if len(paths) == 0: |
| paths = [[node]] |
| return paths |
| |
| all_paths = GetAllPaths(root_node) |
| result = [] |
| for path in all_paths: |
| all_actions_in_path = [] |
| for node in path[1:]: # Skip the root node |
| all_actions_in_path.append(node.action) |
| all_actions_in_path.extend(node.state_check_actions.values()) |
| result.append(CoverageTest(all_actions_in_path, {platform})) |
| return result |
| |
| |
| def generate_coverage_file_and_percents( |
| required_coverage_tests: List[CoverageTest], |
| tested_graph_root: ActionNode, |
| platform: TestPlatform) -> Tuple[str, float, float]: |
| lines = [] |
| total_actions = 0.0 |
| actions_fully_covered = 0.0 |
| actions_partially_covered = 0.0 |
| for coverage_test in required_coverage_tests: |
| action_strings = [] |
| last_action_node = tested_graph_root |
| for action in coverage_test.actions: |
| total_actions += 1 |
| coverage = None |
| if last_action_node is not None: |
| if action.name in last_action_node.children: |
| coverage = action.get_coverage_for_platform(platform) |
| if (action.is_state_check() |
| and last_action_node.has_state_check_action(action)): |
| coverage = action.get_coverage_for_platform(platform) |
| if coverage is None: |
| last_action_node = None |
| action_strings.append(action.name + '🌑') |
| continue |
| elif (coverage == ActionCoverage.FULL): |
| actions_fully_covered += 1 |
| action_strings.append(action.name + '🌕') |
| elif (coverage == ActionCoverage.PARTIAL): |
| action_strings.append(action.name + '🌓') |
| actions_partially_covered += 1 |
| # Only proceed if the action was in the children. If not, then it |
| # was in the stateless action list and we stay at the same node. |
| if action.name in last_action_node.children: |
| last_action_node = last_action_node.children[action.name] |
| lines.append(action_strings) |
| |
| full_coverage = actions_fully_covered / total_actions |
| partial_coverage = ((actions_fully_covered + actions_partially_covered) / |
| total_actions) |
| logging.info(f"Coverage for {platform}:") |
| logging.info(f"Full coverage: {full_coverage:.0%}, " |
| f", with partial coverage: {partial_coverage:.0%}") |
| return ("\n".join(["\t".join(line) |
| for line in lines]), full_coverage, partial_coverage) |