acrn-hypervisor/misc/config_tools/scenario_config/validator.py
Junjie Mao 0a7910c7f0 config_tools: composing operations around XMLs as pipelines
There is an increasing demand of composing different operations around XML
schemas and/or data in different ways for different purpose. Today we
already have:

  - Validate XML data, which takes XML schemas and data (board and
    scenario) as inputs.
  - Fill in missing nodes in XML data with default values, which takes XML
    schema and data (scenario only) as inputs.

In the near future we'll extend the operations around XMLs by introducing
XML schema preprocessing and XML data upgrading, adding more possibilities
to construct a larger operation by composing smaller ones.

In order for minimized code repetition and easier composition, this patch
introduces an infrasturcture that abstracts each operation as a pipeline
stage. Each stage defines its own inputs and outputs and can be composed
sequentially as a larger, single operation.

The existing operations listed above, along with XML file loaders, are then
refactored to provide pipeline stages. The main methods are also refined to
complete their tasks by constructing and invoking pipelines.

Tracked-On: #6690
Signed-off-by: Junjie Mao <junjie.mao@intel.com>
2022-03-15 10:22:37 +08:00

215 lines
8.4 KiB
Python

#!/usr/bin/env python3
#
# Copyright (C) 2022 Intel Corporation.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import sys, os
import argparse
import logging
from copy import copy
from collections import namedtuple
try:
import xmlschema
except ImportError:
logging.error("Python package `xmlschema` is not installed.\n" +
"The scenario XML file will NOT be validated against the schema, which may cause build-time or runtime errors.\n" +
"To enable the validation, install the python package by executing: pip3 install xmlschema.")
sys.exit(0)
from pipeline import PipelineObject, PipelineStage, PipelineEngine
from default_populator import DefaultValuePopulatingStage
def existing_file_type(parser):
def aux(arg):
if not os.path.exists(arg):
parser.error(f"can't open {arg}: No such file or directory")
elif not os.path.isfile(arg):
parser.error(f"can't open {arg}: Is not a file")
else:
return arg
return aux
def log_level_type(parser):
def aux(arg):
arg = arg.lower()
if arg in ["critical", "error", "warning", "info", "debug"]:
return arg
else:
parser.error(f"{arg} is not a valid log level")
return aux
class ValidationError(dict):
def __init__(self, paths, message, severity):
super().__init__(paths = paths, message = message, severity = severity)
def __str__(self):
return f"{', '.join(self['paths'])}: {self['message']}"
class ScenarioValidator:
def __init__(self, schema_etree, datachecks_etree):
"""Initialize the validator with preprocessed schemas in ElementTree."""
self.schema = xmlschema.XMLSchema11(schema_etree)
self.datachecks = xmlschema.XMLSchema11(datachecks_etree)
def check_syntax(self, scenario_etree):
errors = []
it = self.schema.iter_errors(scenario_etree)
for error in it:
# Syntactic errors are always critical.
e = ValidationError([error.path], error.reason, "critical")
logging.debug(e)
errors.append(e)
return errors
def check_semantics(self, board_etree, scenario_etree):
errors = []
unified_node = copy(scenario_etree.getroot())
unified_node.extend(board_etree.getroot())
it = self.datachecks.iter_errors(unified_node)
for error in it:
logging.debug(f"{error.elem}: {error.message}")
anno = error.validator.annotation
severity = anno.elem.get("{https://projectacrn.org}severity")
errors.append(ValidationError([error.elem.tag], error.message, severity))
return errors
class ValidatorConstructionStage(PipelineStage):
# The schema etree may still useful for schema-based transformation. Do not consume it.
uses = {"schema_etree"}
consumes = {"datachecks_etree"}
provides = {"validator"}
def run(self, obj):
validator = ScenarioValidator(obj.get("schema_etree"), obj.get("datachecks_etree"))
obj.set("validator", validator)
class ValidatorConstructionByFileStage(PipelineStage):
uses = {"schema_path", "datachecks_path"}
provides = {"validator"}
def run(self, obj):
validator = ScenarioValidator(obj.get("schema_path"), obj.get("datachecks_path"))
obj.set("validator", validator)
class SyntacticValidationStage(PipelineStage):
uses = {"validator", "scenario_etree"}
provides = {"syntactic_errors"}
def run(self, obj):
errors = obj.get("validator").check_syntax(obj.get("scenario_etree"))
obj.set("syntactic_errors", errors)
class SemanticValidationStage(PipelineStage):
uses = {"validator", "board_etree", "scenario_etree"}
provides = {"semantic_errors"}
def run(self, obj):
errors = obj.get("validator").check_semantics(obj.get("board_etree"), obj.get("scenario_etree"))
obj.set("semantic_errors", errors)
class ReportValidationResultStage(PipelineStage):
consumes = {"board_etree", "scenario_etree", "syntactic_errors", "semantic_errors"}
provides = {"nr_all_errors"}
def run(self, obj):
board_name = obj.get("board_etree").getroot().get("board")
scenario_name = obj.get("scenario_etree").getroot().get("scenario")
nr_critical = len(obj.get("syntactic_errors"))
nr_error = len(list(filter(lambda e: e["severity"] == "error", obj.get("semantic_errors"))))
nr_warning = len(list(filter(lambda e: e["severity"] == "warning", obj.get("semantic_errors"))))
if nr_critical > 0 or nr_error > 0:
logging.error(f"Board {board_name} and scenario {scenario_name} are inconsistent: {nr_critical} syntax errors, {nr_error} data errors, {nr_warning} warnings.")
elif nr_warning > 0:
logging.warning(f"Board {board_name} and scenario {scenario_name} are potentially inconsistent: {nr_warning} warnings.")
else:
logging.info(f"Board {board_name} and scenario {scenario_name} are valid and consistent.")
obj.set("nr_all_errors", nr_critical + nr_error + nr_warning)
def validate_one(validation_pipeline, pipeline_obj, board_xml, scenario_xml):
pipeline_obj.set("board_path", board_xml)
pipeline_obj.set("scenario_path", scenario_xml)
validation_pipeline.run(pipeline_obj)
return pipeline_obj.consume("nr_all_errors")
def validate_board(validation_pipeline, pipeline_obj, board_xml):
board_dir = os.path.dirname(board_xml)
nr_all_errors = 0
for f in os.listdir(board_dir):
if not f.endswith(".xml"):
continue
if f == os.path.basename(board_xml) or "launch" in f:
continue
nr_all_errors += validate_one(validation_pipeline, pipeline_obj, board_xml, os.path.join(board_dir, f))
return nr_all_errors
def validate_all(validation_pipeline, pipeline_obj, data_dir):
nr_all_errors = 0
for f in os.listdir(data_dir):
board_xml = os.path.join(data_dir, f, f"{f}.xml")
if os.path.isfile(board_xml):
nr_all_errors += validate_board(validation_pipeline, pipeline_obj, board_xml)
else:
logging.warning(f"Cannot find a board XML under {os.path.join(data_dir, f)}")
return nr_all_errors
def main(args):
from xml_loader import XMLLoadStage
from lxml_loader import LXMLLoadStage
validator_construction_pipeline = PipelineEngine(["schema_path", "datachecks_path"])
validator_construction_pipeline.add_stages([
LXMLLoadStage("schema"),
LXMLLoadStage("datachecks"),
ValidatorConstructionStage(),
])
validation_pipeline = PipelineEngine(["board_path", "scenario_path", "schema_etree", "validator"])
validation_pipeline.add_stages([
XMLLoadStage("board"),
XMLLoadStage("scenario"),
DefaultValuePopulatingStage(),
SyntacticValidationStage(),
SemanticValidationStage(),
ReportValidationResultStage(),
])
obj = PipelineObject(schema_path = args.schema, datachecks_path = args.datachecks)
validator_construction_pipeline.run(obj)
if args.board and args.scenario:
nr_all_errors = validate_one(validation_pipeline, obj, args.board, args.scenario)
elif args.board:
nr_all_errors = validate_board(validation_pipeline, obj, args.board)
else:
nr_all_errors = validate_all(validation_pipeline, obj, os.path.join(config_tools_dir, "data"))
sys.exit(1 if nr_all_errors > 0 else 0)
if __name__ == "__main__":
config_tools_dir = os.path.join(os.path.dirname(__file__), "..")
schema_dir = os.path.join(config_tools_dir, "schema")
parser = argparse.ArgumentParser()
parser.add_argument("board", nargs="?", type=existing_file_type(parser), help="the board XML file to be validated")
parser.add_argument("scenario", nargs="?", type=existing_file_type(parser), help="the scenario XML file to be validated")
parser.add_argument("--loglevel", default="warning", type=log_level_type(parser), help="choose log level, e.g. debug, info, warning or error")
parser.add_argument("--schema", default=os.path.join(schema_dir, "config.xsd"), help="the XML schema that defines the syntax of scenario XMLs")
parser.add_argument("--datachecks", default=os.path.join(schema_dir, "datachecks.xsd"), help="the XML schema that defines the semantic rules against board and scenario data")
args = parser.parse_args()
logging.basicConfig(level=args.loglevel.upper())
main(args)