65c1d746a7
We don't transfer git history since it may contain proprietary data that we cannot have in an open sources version. Change-Id: I9586124c1720db69a76b9390e208e9f0ba3b86d4
564 lines
22 KiB
Python
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 pybuild.interface.application import Application, Model, get_active_signals
|
|
from pybuild.interface.ems import CsvEMS
|
|
from pybuild.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 pybuild.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 pybuild.check_interface models <Models> --models <ModelOne> <ModelTwo>
|
|
Checks models ModelOne and ModelTwo against all other models in the folder Models
|
|
|
|
py -3.6 -m pybuild.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()
|