Source code for tango_simlib.utilities.validate_device

#########################################################################################
# Copyright 2020 SKA South Africa (http://ska.ac.za/)                                   #
#                                                                                       #
# BSD license - see LICENSE.txt for details                                             #
#########################################################################################
"""This module validates the conformance of a Tango device against a specification"""
from pathlib import Path

import requests
import yaml

from tango_simlib.tango_yaml_tools.base import TangoToYAML
from tango_simlib.utilities.tango_device_parser import TangoDeviceParser

MINIMAL_SPEC_FORMAT = """
class:
meta:
    attributes:
    commands:
    properties:
"""


[docs]def validate_device_from_url(tango_device_name, url_to_yaml_file, bidirectional): """Retrieves the YAML from the URL and checks conformance against the Tango device. Parameters ---------- url_to_yaml_file : str The URL to the specification file tango_device_name : str Tango device name in the domain/family/member format or the FQDN tango://<TANGO_HOST>:<TANGO_PORT>/domain/family/member bidirectional: bool Whether to include details on the device that is not in the specification Returns ------- str The validation result """ response = requests.get(url_to_yaml_file, allow_redirects=True) response.raise_for_status() return compare_data( response.text, get_device_specification(tango_device_name), bidirectional )
[docs]def validate_device_from_path(tango_device_name, path_to_yaml_file, bidirectional): """Retrieves the YAML from the file and checks conformance against the Tango device. Parameters ---------- path_to_yaml_file : str The path to the specification file tango_device_name : str Tango device name in the domain/family/member format or the FQDN tango://<TANGO_HOST>:<TANGO_PORT>/domain/family/member bidirectional: bool Whether to include details on the device that is not in the specification Returns ------- str The validation result """ file_path = Path(path_to_yaml_file) assert file_path.is_file(), "{} is not a file".format(file_path) file_data = "" with open(str(file_path), "r") as data_file: file_data = data_file.read() return compare_data( file_data, get_device_specification(tango_device_name), bidirectional )
[docs]def get_device_specification(tango_device_name): """Translate a device to YAML specification Parameters ---------- tango_device_name : str Tango device name in the domain/family/member format or the FQDN tango://<TANGO_HOST>:<TANGO_PORT>/domain/family/member Returns ------- str The device specification in YAML format """ parser = TangoToYAML(TangoDeviceParser) return parser.build_yaml_from_device(tango_device_name)
[docs]def compare_data(specification_yaml, tango_device_yaml, bidirectional): """Compare 2 sets of YAML built from the specification and from the device Parameters ---------- specification_yaml : str The specification in YAML format tango_device_yaml : str The Tango device in YAML format bidirectional: bool Whether to include details on the device that is not in the specification Returns ------- str The validation result """ validate_spec_structure(specification_yaml) specification_data = yaml.load(specification_yaml, Loader=yaml.FullLoader) tango_device_data = yaml.load(tango_device_yaml, Loader=yaml.FullLoader) if isinstance(specification_data, list): specification_data = specification_data[0] if isinstance(tango_device_data, list): tango_device_data = tango_device_data[0] issues = [] if not specification_data["meta"]["commands"]: specification_data["meta"]["commands"] = [] if not specification_data["meta"]["attributes"]: specification_data["meta"]["attributes"] = [] if not specification_data["meta"]["properties"]: specification_data["meta"]["properties"] = [] # Class if specification_data["class"]: if tango_device_data["class"] != specification_data["class"]: issues.append( "\nClass differs, specified '{}', but device has '{}'".format( specification_data["class"], tango_device_data["class"] ) ) # Commands issues.extend( check_list_dict_differences( specification_data["meta"]["commands"], tango_device_data["meta"]["commands"], "Command", bidirectional, ) ) # Attributes issues.extend( check_list_dict_differences( specification_data["meta"]["attributes"], tango_device_data["meta"]["attributes"], "Attribute", bidirectional, ) ) # Properties issues.extend( check_property_differences( specification_data["meta"]["properties"], tango_device_data["meta"]["properties"], bidirectional, ) ) return "\n".join(issues)
[docs]def check_list_dict_differences(spec_data, dev_data, type_str, bidirectional): """Compare Commands and Attributes in the parsed YAML Parameters ---------- spec_data : list List of dictionaries with specification data E.g [ {'disp_level': 'OPERATOR', 'doc_in': 'ON/OFF', 'doc_out': 'Uninitialised', 'dtype_in': 'DevBoolean', 'dtype_out': 'DevVoid', 'name': 'Capture'}, ... ] dev_data : list List of dictionaries with device data E.g [ {'disp_level': 'OPERATOR', 'doc_in': 'ON/OFF', 'doc_out': 'Uninitialised', 'dtype_in': 'DevBoolean', 'dtype_out': 'DevVoid', 'name': 'Capture'}, ... ] type_str : str Either "Command" or "Attribute" bidirectional: bool Whether to include details on the device that is not in the specification Returns ------- issues : list A list of strings describing the issues, empty list for no issues """ issues = [] spec_data = sorted(spec_data, key=lambda i: i["name"]) dev_data = sorted(dev_data, key=lambda i: i["name"]) # Check that the command/attribute names match spec_data_names = {i["name"] for i in spec_data} dev_data_names = {i["name"] for i in dev_data} if spec_data_names != dev_data_names: diff = spec_data_names.difference(dev_data_names) if diff: diff = sorted(diff) issues.append( "{} differs, [{}] specified but missing in device".format( type_str, ",".join(diff) ) ) if bidirectional: diff = dev_data_names.difference(spec_data_names) if diff: diff = sorted(diff) issues.append( "{} differs, [{}] present in device but not specified".format( type_str, ",".join(diff) ) ) # Check that the commands/attributes (by name) that are in both the spec and device # are the same mutual_names = spec_data_names.intersection(dev_data_names) mutual_spec_data = filter(lambda x: x["name"] in mutual_names, spec_data) mutual_dev_data = filter(lambda x: x["name"] in mutual_names, dev_data) for spec, dev in zip(mutual_spec_data, mutual_dev_data): issues.extend(check_single_dict_differences(spec, dev, type_str, bidirectional)) return issues
[docs]def check_single_dict_differences(spec, dev, type_str, bidirectional): """Compare a single attribute/command Parameters ---------- spec : dict A single Attribute/Command dictionary form the specficication E.g {'disp_level': 'OPERATOR', 'doc_in': 'ON/OFF', 'doc_out': 'Uninitialised', 'dtype_in': 'DevBoolean', 'dtype_out': 'DevVoid', 'name': 'Capture'} dev : dict A single Attribute/Command dictionary form the device E.g {'disp_level': 'OPERATOR', 'doc_in': 'ON/OFF', 'doc_out': 'Uninitialised', 'dtype_in': 'DevBoolean', 'dtype_out': 'DevVoid', 'name': 'Capture'} type_str : str Either "Command" or "Attribute" bidirectional: bool Whether to include details on the device that is not in the specification Returns ------- issues : list A list of strings describing the issues, empty list for no issues """ assert spec["name"] == dev["name"] issues = [] if spec != dev: spec_keys = set(spec.keys()) dev_keys = set(dev.keys()) keys_not_in_spec = spec_keys.difference(dev_keys) keys_not_in_dev = dev_keys.difference(spec_keys) mutual_keys = spec_keys.intersection(dev_keys) if keys_not_in_spec: keys_not_in_spec = sorted(keys_not_in_spec) issues.append( "{} [{}] differs, specification has keys [{}] but it's " "not in device".format(type_str, spec["name"], ",".join(keys_not_in_spec)) ) if keys_not_in_dev and bidirectional: keys_not_in_dev = sorted(keys_not_in_dev) issues.append( "{} [{}] differs, device has keys [{}] but it's " "not in the specification".format( type_str, spec["name"], ",".join(keys_not_in_dev) ) ) for key in mutual_keys: if dev[key] != spec[key]: issues.append("{} [{}] differs:".format(type_str, spec["name"])) issues.append( "\t{}:\n\t\tspecification: {}\n\t\tdevice: {}".format( key, spec[key], dev[key] ) ) return issues
[docs]def check_property_differences(spec_properties, dev_properties, bidirectional): """Compare properties in the parsed YAML Parameters ---------- spec_data : list List of dictionaries with specification data properties E.g [{'name': 'AdminModeDefault'}, {'name': 'AsynchCmdReplyNRetries'}, {'name': 'AsynchCmdReplyTimeout'}, {'name': 'CentralLoggerEnabledDefault'}, {'name': 'ConfigureTaskTimeout'}, {'name': 'ControlModeDefault_B'}] dev_data : list List of dictionaries with device data properties E.g [{'name': 'AdminModeDefault'}, {'name': 'AsynchCmdReplyNRetries'}, {'name': 'AsynchCmdReplyTimeout'}, {'name': 'CentralLoggerEnabledDefault'}, {'name': 'ConfigureTaskTimeout'}, {'name': 'ControlModeDefault_B'}] bidirectional: bool Whether to include details on the device that is not in the specification Returns ------- issues : list A list of strings describing the issues, empty list for no issues """ issues = [] spec_props = {i["name"] for i in spec_properties} dev_props = {i["name"] for i in dev_properties} if spec_props != dev_props: diff = spec_props.difference(dev_props) if diff: diff = sorted(diff) issues.append( "Property [{}] differs, specified but missing in device".format( ",".join(diff) ) ) if bidirectional: diff = dev_props.difference(spec_props) if diff: diff = sorted(diff) issues.append( "Property [{}] differs, present in device but not specified".format( ",".join(diff) ) ) return issues
[docs]def validate_spec_structure(specification_yaml): """Make sure that the minimal specification structure is adhered to. Parameters ---------- specification_yaml : str The specification in YAML format """ specification_data = yaml.load(specification_yaml, Loader=yaml.FullLoader) if isinstance(specification_data, list): specification_data = specification_data[0] passes = True if "class" not in specification_data: passes = False if "meta" not in specification_data: passes = False else: for name in ["commands", "attributes", "properties"]: if name not in specification_data["meta"]: passes = False if not passes: assert 0, "Minimal structure not adhered to:\n{}".format(MINIMAL_SPEC_FORMAT) for type_str in ["commands", "attributes", "properties"]: if specification_data["meta"][type_str]: for type_dict in specification_data["meta"][type_str]: assert "name" in type_dict, "`name` field is required for all {}".format( type_str )