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