powertrain-build/powertrain_build/check_interface.py
olindgre 2ece01e1d7 Make powertrain-build not overlap with pybuild in site-packages
Change-Id: I7b59f3f04f0f787d35db0b9389f295bf1ad24f56
2024-09-17 10:25:04 +02:00

564 lines
22 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
# -*- coding: utf-8 -*-
"""Python module used for calculating interfaces for CSP"""
import argparse
import os
import re
import sys
from itertools import product
from pathlib import Path
import git
from powertrain_build.interface.application import Application, Model, get_active_signals
from powertrain_build.interface.ems import CsvEMS
from powertrain_build.lib import logger
LOGGER = logger.create_logger("Check interface")
def process_app(config):
"""Get an app specification for the current project
Entrypoint for external scripts.
Args:
config (pathlib.Path): Path to the ProjectCfg.json
Returns:
app (Application): pybuild project
"""
app = Application()
app.parse_definition(config)
return app
def model_app_consistency(model, app_models, app, errors):
"""Compare model signal interface with list of models.
Args:
model (Model): model to compare against application
app_models (list(Model)): list of models to compare with
app (Application): pybuild project
errors (dict): Object for counting errors of different types
"""
for compare_model in app_models:
LOGGER.debug("Comparing %s with %s in %s", model.name, compare_model.name, app.name)
active_model_outsignals = get_active_signals(model.outsignals, app.pybuild["feature_cfg"])
active_model_insignals = get_active_signals(model.insignals, app.pybuild["feature_cfg"])
active_compare_outsignals = get_active_signals(compare_model.outsignals, app.pybuild["feature_cfg"])
active_compare_insignals = get_active_signals(compare_model.insignals, app.pybuild["feature_cfg"])
check_signals(
active_model_insignals,
active_compare_outsignals,
errors,
[app.name, model.name],
[app.name, compare_model.name],
)
check_signals(
active_model_outsignals,
active_compare_insignals,
errors,
[app.name, model.name],
[app.name, compare_model.name],
)
def check_internal_signals(app, model_names=None):
"""Look for all internal signal mismatches.
Args:
app (Application): pybuild project
model_names (list(Model)): models based on parsed config jsons
Returns:
serious_mismatch (bool): A serious mismatch was found
"""
serious_mismatch = False
LOGGER.debug("Checking internal signals")
LOGGER.debug("Checking against %s", app.signals)
errors = {"type": 0, "range": 0, "unit": 0, "width": 0}
app_models = app.get_models()
for signal in app.signals:
LOGGER.debug(signal.properties)
for model in app_models:
if model_names is not None and model.name not in model_names:
LOGGER.debug("Skipping %s", model.name)
continue
LOGGER.debug("Checking %s in %s", model.name, app.name)
active_insignals = get_active_signals(model.insignals, app.pybuild["feature_cfg"])
insignal_mismatch = check_signals(active_insignals, app.signals, errors, [app.name, model.name], [app.name])
active_outsignals = get_active_signals(model.outsignals, app.pybuild["feature_cfg"])
outsignal_mismatch = check_signals(active_outsignals, app.signals, errors, [app.name, model.name], [app.name])
if insignal_mismatch or outsignal_mismatch:
serious_mismatch = True
model_app_consistency(model, app_models, app, errors)
# Only compare with all models if a mismatch is found
LOGGER.debug("Total errors: %s", errors)
return serious_mismatch
def check_models_generic(all_models, model_names, emses):
"""Check filtered models against all models and external interfaces."""
serious_mismatch = False
for model in all_models:
LOGGER.info("Checking signals attributes for %s", model.name)
if model.name not in model_names:
continue
errors = {"type": 0, "range": 0, "unit": 0, "width": 0}
LOGGER.debug("Checking internal signals for %s", model.name)
for corresponding_model in all_models:
serious_mismatch |= check_signals(
model.insignals, corresponding_model.outsignals, errors, [model.name], [corresponding_model.name]
)
serious_mismatch |= check_signals(
model.outsignals, corresponding_model.insignals, errors, [model.name], [corresponding_model.name]
)
if emses:
LOGGER.debug("Checking external signals for %s", model.name)
for ems in emses:
serious_mismatch |= check_signals(
model.insignals, ems.outsignals, errors, [model.name], [ems.name]
)
serious_mismatch |= check_signals(
model.outsignals, ems.insignals, errors, [model.name], [ems.name]
)
LOGGER.debug("Total errors for %s: %s", model.name, errors)
return serious_mismatch
def get_all_models(model_root):
"""Find, filter and parse all model configurations."""
LOGGER.info("Parsing all models")
prefix = "config_"
suffix = ".json"
models = []
for dirpath, _, filenames in os.walk(model_root):
dirpath = Path(dirpath)
for filename in [f for f in filenames if f.startswith(prefix) and f.endswith(suffix)]:
name = filename[len(prefix): -len(suffix)]
if name == dirpath.parent.stem:
model = Model(None)
model.parse_definition((name, Path(dirpath, filename)))
models.append(model)
return models
def get_projects(root, project_names):
"""Find, parse and filter all project configurations."""
LOGGER.info("Parsing all projects")
projects = []
for dirpath, _, filenames in os.walk(root):
dirpath = Path(dirpath)
for filename in [f for f in filenames if f == "ProjectCfg.json"]:
config = Path(dirpath, filename)
app = Application()
app_name = app.get_name(config)
if project_names is not None and app_name not in project_names:
if config.parent.stem not in project_names:
LOGGER.info("%s or %s does not match %s", app_name, config.parent.stem, project_names)
continue
app.parse_definition(config)
if app.pybuild["build_cfg"].has_yaml_interface:
LOGGER.warning("Interface checks for yaml-interface projects are not implemtented yet")
LOGGER.info("Adding empty interface for %s", app_name)
projects.append((app, None))
else:
ems = CsvEMS()
ems.parse_definition(config)
projects.append((app, ems))
return projects
def correct_type(left_spec, right_spec):
"""Check if the type is the same in two specifications.
Args:
left_spec (dict): Signal specification
right_spec (dict): Signal specification to compare with
Returns:
matches (bool): Spec1 and Spec2 has the same type
"""
return left_spec["type"] == right_spec["type"]
def correct_attribute(left_spec, right_spec, attribute, default=None, check_bool=True):
"""Check attributes other than type.
Args:
left_spec (dict): Signal specification
right_spec (dict): Signal specification to compare with
attribute (string): Attribute to check
default (value): Default value for the attribute (default: None)
check_bool (bool): Check signals of type Bool (default: True)
Returns:
matches (bool): Spec1 and Spec2 has the same value for the attribute
"""
def _format(value):
if isinstance(value, str):
value = value.strip()
if re.fullmatch("[+-]?[0-9]+", value):
value = int(value)
elif re.fullmatch("[+-]?[0-9]+[0-9.,eE+]*", value):
value = float(value.replace(",", "."))
return value
if not check_bool and left_spec["type"] == "Bool":
return True
return _format(left_spec.get(attribute, default)) == _format(right_spec.get(attribute, default))
def found_mismatch(name, left_spec, right_spec, attribute, left_path, right_path):
"""Handle finding a mismatch.
Args:
name (string): Name of signal
left_spec (dict): Spec of signal
right_spec (dict): Signal specification to compare with
attribute (string): Attribute to check
left_path (list(str)): Path for where the left signals' definitions come from
right_path (list(str)): Path for where the right signals' definitions come from
"""
if attribute in ["type", "width"]:
# TODO: Add more properties as serious when the interfaces are more cleaned up
LOGGER.error(
"%s has %ss: %s in %s and %s in %s",
name,
attribute,
left_spec.get(attribute),
left_path,
right_spec.get(attribute),
right_path,
)
return True
LOGGER.info(
"%s has %ss: %s in %s and %s in %s",
name,
attribute,
left_spec.get(attribute),
left_path,
right_spec.get(attribute),
right_path,
)
return False
def check_external_signals(ems, app, model_names=None):
"""Look for external signal mismatches.
Args:
ems (CsvEMS): Parsed signal interface cvs:s
app (Application): Parsed project config
model_names (list(Model)): models based on parsed config jsons
Returns:
serious_mismatch (bool): A serious mismatch was found
"""
serious_mismatch = False
LOGGER.debug("Checking insignals")
errors = {"type": 0, "range": 0, "unit": 0, "width": 0}
app_models = app.get_models()
for model in app_models:
if model_names is not None and model.name not in model_names:
LOGGER.debug("Skipping %s in %s", model.name, app.name)
continue
LOGGER.debug("Checking %s in %s", model.name, app.name)
serious_mismatch |= check_signals(
get_active_signals(model.insignals, app.pybuild["feature_cfg"]),
ems.outsignals,
errors,
[app.name, model.name],
[ems.name],
)
serious_mismatch |= check_signals(
get_active_signals(model.outsignals, app.pybuild["feature_cfg"]),
ems.insignals,
errors,
[app.name, model.name],
[ems.name],
)
LOGGER.debug("Total errors: %s", errors)
return serious_mismatch
def check_signals(left_signals, right_signals, errors, left_path=None, right_path=None):
"""Compares insignals from one system with the outsignals of another.
Args:
left_signals (list(Signal)): Insignals of one system such as a model
right_signals (list(Signal)): Outsignals of system to compare with
errors (dict): Object for counting errors of different types
left_path (list(str)): Path for where the left signals' definitions come from
right_path (list(str)): Path for where the right signals' definitions come from
Returns:
serious_mismatch (bool): A serious mismatch was found
"""
left_path = [] if left_path is None else left_path
right_path = [] if right_path is None else right_path
serious_mismatch = False
LOGGER.debug("Checking from %s", left_signals)
LOGGER.debug("Checking against %s", right_signals)
for (left_signal, right_signal) in [
(left, right) for left, right in product(left_signals, right_signals) if left.name == right.name
]:
LOGGER.debug("Comparing %s and %s", left_signal, right_signal)
left_properties = left_signal.properties
right_properties = right_signal.properties
LOGGER.debug("Properties left: %s", left_properties)
LOGGER.debug("Properties right: %s", right_properties)
if not correct_type(left_properties, right_properties):
serious_mismatch |= found_mismatch(
left_signal.name, left_properties, right_properties, "type", left_path, right_path
)
errors["type"] += 1
if not correct_attribute(left_properties, right_properties, "min", check_bool=False):
serious_mismatch |= found_mismatch(
left_signal.name, left_properties, right_properties, "min", left_path, right_path
)
errors["range"] += 1
if not correct_attribute(left_properties, right_properties, "max", check_bool=False):
serious_mismatch |= found_mismatch(
left_signal.name, left_properties, right_properties, "max", left_path, right_path
)
errors["range"] += 1
if not correct_attribute(left_properties, right_properties, "unit", default="", check_bool=False):
serious_mismatch |= found_mismatch(
left_signal.name, left_properties, right_properties, "unit", left_path, right_path
)
errors["unit"] += 1
if not correct_attribute(left_properties, right_properties, "width", default=1):
serious_mismatch |= found_mismatch(
left_signal.name, left_properties, right_properties, "width", left_path, right_path
)
errors["width"] += 1
return serious_mismatch
def parse_args():
"""Parse arguments
Returns:
Namespace: the parsed arguments
"""
parser = argparse.ArgumentParser(
description="""
Checks attributes and existence of signals
Produced but not consumed signals are giving warnings
Consumed but not produced signals are giving errors
Attributes checked are: types, ranges, units and widths
Mismatches in types or widths give errors
Mismatches in min, max or unit gives warnings
Examples:
py -3.6 -m powertrain_build.check_interface models_in_projects <Projects> <Models/ModelGroup>\
--projects <ProjectOne> <ProjectTwo>
Checks models in Models/ModelGroup against ProjectOne and ProjectTwo in the folder Projects
py -3.6 -m powertrain_build.check_interface models <Models> --models <ModelOne> <ModelTwo>
Checks models ModelOne and ModelTwo against all other models in the folder Models
py -3.6 -m powertrain_build.check_interface projects <Projects> \
--projects ProjectOne ProjectTwo ProjectThree
Checks the interfaces of ProjectOne, ProjectTwo and ProjectThree in the folder Projects
""",
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers(help="help for subcommand", dest="mode")
# create the parser for the different commands
model = subparsers.add_parser(
"models",
description="""
Check models independently of projects.
All signals are assumed to be active.
Any signal that gives and error is used in a model but is not produced in any model or project
interface.
""",
)
add_model_args(model)
project = subparsers.add_parser(
"projects",
description="""
Check projects as a whole.
It checks all models intenally and the SPM vs the interface.
""",
)
add_project_args(project)
models_in_projects = subparsers.add_parser(
"models_in_projects",
description="""
Check models specifically for projects.
Codeswitches are used to determine if the signals are produced and consumed in each model.
""",
)
add_project_args(models_in_projects)
add_model_args(models_in_projects)
models_in_projects.add_argument("--properties", help="Check properties such as type", action="store_true")
models_in_projects.add_argument("--existence", help="Check signal existence consistency", action="store_true")
return parser.parse_args()
def add_project_args(parser):
"""Add project arguments to subparser"""
parser.add_argument("project_root", help="Path to start looking for projects", type=Path)
parser.add_argument(
"--projects", help="Name of projects to check. Matches both path and interface name.", nargs="+"
)
def add_model_args(parser):
"""Add model arguments to subparser"""
parser.add_argument("model_root", help="Path to start looking for models", type=Path)
parser.add_argument("--models", help="Name of models to check", nargs="+")
parser.add_argument("--gerrit", action="store_true", help="Deprecated")
parser.add_argument("--git", action="store_true", help="Get models to check from git HEAD")
def get_changed_models():
"""Get changed models in current commit."""
repo = git.Repo()
changed_files_tmp = repo.git.diff("--diff-filter=d", "--name-only", "HEAD~1")
changed_files = changed_files_tmp.splitlines()
changed_models = [m for m in changed_files if m.endswith(".mdl") or m.endswith(".slx")]
return changed_models
def model_path_to_name(model_paths):
"""Extract model names from a list of model paths."""
model_names = []
for model_path in model_paths:
model_name_with_extension = model_path.split("/")[-1]
model_name = model_name_with_extension.split(".")[0]
model_names.append(model_name)
return model_names
def model_check(args):
"""Entry point for models command."""
serious_mismatch = False
all_models = get_all_models(args.model_root)
if args.models is not None:
model_names = args.models
elif args.git or args.gerrit:
# Still checking args.gerrit due to common-linux-signal_consistency in pt-zuul-jobs
model_paths = get_changed_models()
model_names = model_path_to_name(model_paths)
else:
model_names = [model.name for model in all_models]
serious_mismatch |= check_models_generic(all_models, model_names, [])
return serious_mismatch
def projects_check(args):
"""Entry point for projects command."""
serious_mismatch = False
projects = get_projects(args.project_root, args.projects)
for app, ems in projects:
LOGGER.info("Checking interfaces for %s", app.name)
serious_mismatch |= check_internal_signals(app, None)
if ems is not None:
serious_mismatch |= check_external_signals(ems, app, None)
return serious_mismatch
def models_in_projects_check(args):
"""Entry point for models_in_projects command."""
serious_mismatch = False
projects = get_projects(args.project_root, args.projects)
LOGGER.debug("Checking projects: %s", projects)
if args.properties:
for app, ems in projects:
serious_mismatch |= check_internal_signals(app, None or args.models)
if ems is not None:
serious_mismatch |= check_external_signals(ems, app, None or args.models)
if args.existence:
all_models = get_all_models(args.model_root)
model_names = [model.name for model in all_models] if args.models is None else args.models
serious_mismatch |= signal_existence(projects, model_names)
return serious_mismatch
def signal_existence(projects, model_names):
"""Check which signals are consumed and produced in each project."""
serious_mismatch = False
for app, ems in projects:
app_models = app.get_models()
LOGGER.info("Checking %s", app.name)
for project_model in app_models:
if project_model.name not in model_names:
continue
LOGGER.debug("Checking signal existence for %s", project_model.name)
active_insignals = get_active_signals(project_model.insignals, app.pybuild["feature_cfg"])
active_outsignals = get_active_signals(project_model.outsignals, app.pybuild["feature_cfg"])
insignal_matches = {}
outsignal_matches = {}
for check_model in app_models:
signal_match(
active_insignals,
get_active_signals(check_model.outsignals, app.pybuild["feature_cfg"]),
insignal_matches,
)
signal_match(
active_outsignals,
get_active_signals(check_model.insignals, app.pybuild["feature_cfg"]),
outsignal_matches,
)
if ems is not None:
signal_match(active_insignals, ems.outsignals, insignal_matches)
signal_match(active_outsignals, ems.insignals, outsignal_matches)
for missing_signal in [signal for signal, matched in insignal_matches.items() if not matched]:
# serious_mismatch = True # TODO: Activate this code when we want to gate on it.
LOGGER.warning(
"%s is consumed in %s but never produced in %s", missing_signal, project_model.name, app.name
)
for missing_signal in [signal for signal, matched in insignal_matches.items() if not matched]:
LOGGER.debug("%s is consumed in %s and produced in %s", missing_signal, project_model.name, app.name)
for missing_signal in [signal for signal, matched in outsignal_matches.items() if not matched]:
LOGGER.info(
"%s is produced in %s but never consumed in %s", missing_signal, project_model.name, app.name
)
for missing_signal in [signal for signal, matched in outsignal_matches.items() if not matched]:
LOGGER.debug("%s is consumed in %s and produced in %s", missing_signal, project_model.name, app.name)
return serious_mismatch
def signal_match(signals_to_check, signals_to_check_against, matches):
"""Check for matches in signal names."""
for a_signal in signals_to_check:
matches[a_signal.name] = matches.get(a_signal.name, False)
for b_signal in signals_to_check_against:
if b_signal.name == a_signal.name:
matches[a_signal.name] = True
def main():
"""Main function for stand alone execution."""
args = parse_args()
if args.mode == "models":
serious_errors = model_check(args)
if args.mode == "projects":
serious_errors = projects_check(args)
if args.mode == "models_in_projects":
serious_errors = models_in_projects_check(args)
if serious_errors:
LOGGER.error("Serious interface errors found.")
sys.exit(1)
if __name__ == "__main__":
main()