diff --git a/yasd.py b/yasd.py index 2e7ce97..f954fbd 100755 --- a/yasd.py +++ b/yasd.py @@ -25,6 +25,7 @@ class YASD: action="check", indata=None, outfile=None, + tests=None, quiet=False, log=False, stdout=False, @@ -40,6 +41,8 @@ class YASD: :type indata: None or Path or dict :param outfile: YASD output file path; 'None' by default :type outfile: None or Path + :param tests: XML tests; 'None' by default + :type tests: None or list :param quiet: If messages are print or not; 'False' by default :type quiet: True or False :param log: If messages are write in a file or not; 'False' by default @@ -52,7 +55,7 @@ class YASD: or 'sample'; YAML dict on 'check' :rtype: str or bs4.element.Tag or dict """ - yasd = YASD(indata, outfile, quiet, log, stdout, validate) + yasd = YASD(indata, outfile, tests, quiet, log, stdout, validate) yasd.msgr.run(f"action_{action}") match action: case "document": @@ -68,6 +71,7 @@ class YASD: self, indata=None, outfile=None, + tests=None, quiet=False, log=False, stdout=False, @@ -80,6 +84,8 @@ class YASD: :type indata: None or Path or dict :param outfile: YASD output file path; 'None' by default :type outfile: None or Path + :param tests: XML tests; 'None' by default + :type tests: None or list :param quiet: If messages are print or not; 'False' by default :type quiet: True or False :param log: If messages are write in a file or not; 'False' by default @@ -94,6 +100,7 @@ class YASD: self.formatter = XMLFormatter(indent=2) self.stdout = stdout self.validate = validate + self.tests = set(tests) if tests is not None else None if outfile is None: self.outfile = None else: @@ -107,11 +114,11 @@ class YASD: :rtype: bs4.element.Tag """ self.xsd = YASDXSD(self.yaml, self.msgr).xsd - out = self.__output(self.xsd) + out = self.__output(self.xsd, self.msgr) if self.validate: - return YASDCheck.xsd(out, self.msgr) + return YASDCheck.xsd(out, self.tests, self.msgr) else: - return out + return {"xsd": out, "tests": []} def sample(self): """ @@ -121,7 +128,7 @@ class YASD: :rtype: bs4.element.Tag """ self.xml = YASDXML(self.yaml, self.msgr).xml - return self.__output(self.xml, extname=".xml") + return self.__output(self.xml, self.msgr, extname=".xml") def document(self): """ @@ -131,21 +138,26 @@ class YASD: :rtype: str """ self.rst = YASDRST(self.yaml, self.msgr).rst - return self.__output(self.rst, extname=".rst") + return self.__output(self.rst, self.msgr, extname=".rst") - def __output(self, outdata, extname=".xsd"): + def __output(self, outdata="", msgr=None, extname=".xsd"): """ Prints in the terminal or writes into a file. + :param outdata: Data for output + :type outdata: bs4.BeautifulSoup or str + :param messenger: Messenger object + :type messenger: None or YASDMessenger + :param str extname: Extension name for file output :return: Output data :rtype: str """ - if type(outdata) is not str: + if not isinstance(outdata, str): outdata = outdata.prettify(formatter=self.formatter) outdata = f'\n{outdata}' if self.stdout: if self.outfile is None: - print(outdata) + msgr.run(outdata) else: suffix = self.outfile.suffix if len(suffix) > 0 and suffix == suffix.replace(" ", ""): @@ -237,6 +249,8 @@ class YASDXSD: del el["fixed"] if "restriction" in el.keys() and "datatype" in el.keys(): del el["datatype"] + if "type" in el.keys() and tag == "element": + del el["type"] for key, val in el.items(): if key == "datatype": element["type"] = f"xs:{val}" @@ -518,22 +532,134 @@ class YASDCheck: except Exception: msgr.run("no_url", level="error", url=url) - def xsd(xsd, msgr=None): + def xsd(xsd, tests=None, msgr=None): """ Validates XSD. :param str xsd: XSD as XML string + :param tests: XML file paths + :type tests: None or set :param msgr: Messenger object :type msgr: None or YASDMessenger + :return: XSD as string and results as list + :rtype: dict """ msgr = YASDCheck.messenger(msgr) msgr.run("validating") try: - xmlschema.XMLSchema(xsd) + valid_xsd = xmlschema.XMLSchema(xsd) + tests = YASDCheck.xsd_test(valid_xsd, tests, msgr) + return {"xsd": xsd, "tests": tests} except xmlschema.validators.exceptions.XMLSchemaParseError as error: error = str(error).replace("\n", "\n ") msgr.run("no_valid", error=error, level="error") + def xsd_test(xsd, tests=None, msgr=None): + """ + Test XSD against XML files. + + :param xsd: XSD for testing + :type xsd: XMLSchema10 or str + :param tests: XML file paths + :type tests: None or set + :param msgr: Messenger object + :type msgr: None or YASDMessenger + :return: Tests results + :rtype: list + """ + results = [] + if isinstance(xsd, str): + xsd = xmlschema.XMLSchema(xsd) + if isinstance(tests, set): + for test in tests: + result = YASDCheck.__valid_test_path(test) + if "error" not in result.keys(): + result = YASDCheck.__valid_test_xml(xsd, result) + results.append(result) + YASDCheck.__print_tests(results, msgr) + return results + + def __valid_test_path(filepath=""): + """ + Validates test path. + + :param filepath: Test file path + :type filepath: Path or str + """ + result = {"path": filepath} + if isinstance(filepath, str): + filepath = Path(filepath) + if not filepath.exists() or not filepath.is_file(): + if not filepath.exists(): + error = YASDMessenger.keys()["no_exists"] + else: + error = YASDMessenger.keys()["no_file"] + result.setdefault("msg", error) + result.setdefault("error", True) + return result + + def __valid_test_xml(xsd, result): + """ + Test XSD against XML file. + + The result is a temporary dict with only 'path' as key. + + :param XMLSchema10 xsd: XSD for testing + :param dict result: Temporary test result + :return: Test result + :rtype: dict + """ + try: + xsd.validate(result["path"]) + result.setdefault("msg", YASDMessenger.keys()["passed"]) + result.setdefault("error", False) + except Exception as error: + result.setdefault("msg", error.message) + result.setdefault("error", True) + return result + + def __print_tests(results=[], msgr=None): + """ + Prints XSD test results. + + :param list results: Tests results + :param msgr: Messenger object + :type msgr: None or YASDMessenger + """ + for result in results: + level = "warn" if result["error"] else "info" + res = "FAILED" if result["error"] else "PASSED" + msg = (" " * 10) + result["msg"] + expectation = YASDCheck.__get_expectation(result["path"]) + msgr.run("testing", path=result["path"]) + msgr.run( + "test_result", + level=level, + path=result["path"], + expectation=expectation, + res=res, + msg=msg, + ) + + def __get_expectation(filepath=""): + """ + Gets XML file expected result. + + :param filepath: Test file path + :type filepath: Path or str + :return: 'PASSED' or 'FAILED' or '???' + :rtype: str + """ + if isinstance(filepath, str): + filepath = Path(filepath) + name = filepath.stem + if name.find("pass") >= 0: + return "PASSED" + elif name.find("fail") >= 0: + return "FAILED" + else: + return "???" + def __init__(self, indata=None, messenger=None): """ Inits YASD validator. @@ -597,11 +723,22 @@ class YASDMessenger: Academy of Language . Licensed under GPLv3 . """, - "readme": "https://gitlab.com/perrotuerto_personal/codigo/yasd/-/raw/no-masters/README.md", + "readme": "".join( + [ + "https://gitlab.com/perrotuerto_personal/codigo/yasd/", + "-/raw/no-masters/README.md", + ] + ), "w3": "https://www.w3.org/2001/XMLSchema.xsd", "help_action": "action to perform", "help_input": "input file in YAML format", "help_output": "output file", + "help_tests": " ".join( + [ + "one or more XML test files;", + "use 'pass' or 'fail' as file name prefix for expectation", + ] + ), "help_quiet": "enable quiet mode", "help_log": "write log", "action_convert": "creating XSD schema", @@ -615,8 +752,20 @@ class YASDMessenger: "no_input": "input file needed", "no_yaml": "YAML dict needed", "no_valid": "XSD schema has the following error:\n @error", + "no_exists": "File doesn't exists", + "no_file": "Path isn't a file", "fetching": "fetching '@url'", "validating": "validating XSD schema", + "testing": "testing XSD against '@path'", + "test_result": "\n".join( + [ + "test output:", + " File: @path", + " Expectation: @expectation", + " Result: @res\n@msg", + ] + ), + "passed": "Test passed!", } def __init__(self, quiet=False, log=False): @@ -703,6 +852,7 @@ class YASDCLI: args.action, args.input, args.output, + args.tests, args.quiet, args.log, stdout=True, @@ -744,6 +894,15 @@ class YASDCLI: default=None, help=msg["help_output"], ) + self.parser.add_argument( + "-t", + "--tests", + type=Path, + default=None, + help=msg["help_tests"], + action="extend", + nargs="+", + ) if __name__ == "__main__":