"""
Conditions represent a build configuration under which to disable a test.
This file contains a canonical list of conditions and their properties, and code
for composing, parsing, and simplifying them. This is independent of their
representation within any particular test format.
"""
import collections
import functools
import itertools
import types
from typing import List, Optional, Union, Set, Tuple
import errors
class BaseCondition:
"""BaseCondition is a class for sentinel values ALWAYS and NEVER."""
ALWAYS = BaseCondition()
NEVER = BaseCondition()
@functools.total_ordering
class Terminal:
"""A boolean variable, the value of which depends on the configuration."""
def __init__(self, name: str, group: Optional[str]):
"""
Args:
name: The generic name for this terminal. Used to specify conditions on
the command line.
group: The group to which this condition belongs. Every Terminal with the
same group is mutually exclusive. For example, "os" - we can't be
compiling for Linux and Mac at the same time.
We also add fields for each test format, initialised to None. It's up to the
file defining the relevant code to fill these out.
"""
self.name: str = name
self.group: Optional[str] = group
self.gtest_info = None
self.expectations_info = None
def __str__(self):
return (f"Terminal('{self.name}')")
def __repr__(self):
return str(self)
def __lt__(self, other):
"""Define a consistent ordering for conditions written into test files"""
if not isinstance(other, Terminal):
return False
if (self.group is not None) == (other.group is not None):
return (self.group, self.name) < (other.group, other.name)
return self.group is not None
def __eq__(self, other):
return isinstance(other, Terminal) and self.name == other.name
def __hash__(self):
return hash(self.name)
TERMINALS = [
Terminal('android', group='os'),
Terminal('chromeos', group='os'),
Terminal('fuchsia', group='os'),
Terminal('ios', group='os'),
Terminal('linux', group='os'),
Terminal('mac', group='os'),
Terminal('win', group='os'),
Terminal('arm64', group='arch'),
Terminal('x86', group='arch'),
Terminal('x86-64', group='arch'),
Terminal('asan', group=None),
Terminal('msan', group=None),
Terminal('tsan', group=None),
]
assert len({t.name for t in TERMINALS}) == len(TERMINALS)
Condition = Union[BaseCondition, tuple, Terminal]
def get_term(name: str) -> Terminal:
"""Look up a Terminal by name."""
t = next((t for t in TERMINALS if t.name == name), None)
if t is not None:
return t
raise ValueError(f"Unknown condition '{name}'")
def parse(condition_strs: List[str]) -> Condition:
"""Parse a list of condition strings, as passed on the command line.
Each element of condition_strs is a set of Terminal names joined with '&'s.
The list is implicitly 'or'ed together.
"""
if not condition_strs:
return ALWAYS
try:
return op_of('or', [
op_of('and', [get_term(x.strip()) for x in cond.split('&')])
for cond in condition_strs
])
except ValueError as e:
valid_conds = '\n'.join(sorted(f'\t{term.name}' for term in TERMINALS))
raise errors.UserError(f"{e}\nValid conditions are:\n{valid_conds}")
def op_of(op: str, args: List[Condition]) -> Condition:
"""Make an operator, simplifying the single-argument case."""
if len(args) == 1:
return args[0]
return (op, args)
def merge(existing_cond: Condition, new_cond: Condition) -> Condition:
"""Merge two conditions together.
Given an existing condition, parsed from a file, and a new condition to be
added, combine the two to produce a merged condition.
"""
if existing_cond == ALWAYS:
return new_cond
if existing_cond == NEVER:
return new_cond
if new_cond == ALWAYS:
return ALWAYS
if new_cond == NEVER:
return NEVER
cond = ('or', [existing_cond, new_cond])
return simplify(cond)
def generate_condition_groups(terms: List[Terminal]) -> List[Set[Terminal]]:
"""Partition a list of Terminals by their 'group' attribute."""
by_group = collections.defaultdict(set)
ungrouped = []
for term in terms:
if term.group is not None:
by_group[term.group].add(term)
else:
ungrouped.append({term})
groups = list(by_group.values())
groups += ungrouped
return groups
CONDITION_GROUPS = generate_condition_groups(TERMINALS)
def simplify(cond: Condition) -> Condition:
"""Given a Condition, produce an equivalent but simpler Condition.
This function uses the Quine-McCluskey algorithm. It's not implemented very
efficiently, but it works and is fast enough for now.
"""
if isinstance(cond, BaseCondition):
return cond
DONT_CARE = 2
variables = list(sorted(find_terminals(cond)))
dont_cares = []
min_terms = []
for possible_input in itertools.product([0, 1], repeat=len(variables)):
true_vars = {variables[i] for i, val in enumerate(possible_input) if val}
if any(len(group & true_vars) > 1 for group in CONDITION_GROUPS):
dont_cares.append(possible_input)
elif evaluate(cond, true_vars):
min_terms.append(possible_input)
combined_some_minterms = True
prev_round = set(min_terms + dont_cares)
prime_implicants: List[Tuple] = []
while combined_some_minterms:
new_implicants = set()
used = set()
combined_some_minterms = False
for a, b in itertools.combinations(prev_round, 2):
diff_index = None
for i, (x, y) in enumerate(zip(a, b)):
if x != y:
if diff_index is not None:
break
diff_index = i
else:
if diff_index is not None:
new_implicants.add(a[:diff_index] + (DONT_CARE, ) +
a[diff_index + 1:])
used |= {a, b}
combined_some_minterms = True
prime_implicants.extend(prev_round - used)
prev_round = new_implicants
or_args: List[Condition] = []
for pi in sorted(prime_implicants):
and_args: List[Condition] = []
for i, x in enumerate(pi):
if x == DONT_CARE:
continue
var = variables[i]
if x == 0:
and_args.append(('not', var))
else:
assert x == 1
and_args.append(var)
or_args.append(op_of('and', and_args))
return op_of('or', or_args)
def find_terminals(cond: Condition) -> Set[Terminal]:
"""Find all leaf Terminal nodes of this Condition."""
if isinstance(cond, BaseCondition):
return set()
if isinstance(cond, Terminal):
return {cond}
assert isinstance(cond, tuple)
op, args = cond
if op == 'not':
return find_terminals(args)
return {var for arg in args for var in find_terminals(arg)}
def evaluate(cond: Condition, true_vars: Set[Terminal]) -> bool:
"""Evaluate a condition with a given set of true variables."""
if isinstance(cond, BaseCondition):
return cond is ALWAYS
if isinstance(cond, Terminal):
return cond in true_vars
op, args = cond
if op == 'not':
return not evaluate(args, true_vars)
return {'or': any, 'and': all}[op](evaluate(arg, true_vars) for arg in args)