chromium/chrome/test/webapps/graph_analysis.py

#!/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)