#!/usr/bin/python3 import argparse import os import sys import subprocess import traceback import yaml run_pycodestyle = False try: import pycodestyle run_pycodestyle = True except ImportError as e: print(e) print("Install pycodestyle: pip3 install pycodestyle") sys.exit(1) try: import magic except ImportError as e: print(e) print("Install python-magic: pip3 install python-magic") sys.exit(1) def print_stderr(message): sys.stderr.write(message) sys.stderr.write("\n") def publish_result(result_message_list, args): if result_message_list: result_message = '\n'.join(result_message_list) try: f = open(args.result_file, 'a') f.write(result_message) f.write("\n") f.close() except IOError as e: print(e) print_stderr("Cannot write to result file: %s" % args.result_file) if args.verbose: print_stderr(result_message) else: result_message = '\n'.join(args.failed_message_list) print_stderr(result_message) def detect_abi(): # Retrieve the current canonical abi from # automated/lib/sh-test-lib:detect_abi return subprocess.check_output( ". automated/lib/sh-test-lib && detect_abi && echo $abi", shell=True).decode('utf-8').strip() def pycodestyle_check(filepath, args): _fmt = "%(row)d:%(col)d: %(code)s %(text)s" options = { 'ignore': args.pycodestyle_ignore, "show_source": True} pycodestyle_checker = pycodestyle.StyleGuide(options) fchecker = pycodestyle_checker.checker_class( filepath, options=pycodestyle_checker.options) fchecker.check_all() if fchecker.report.file_errors > 0: result_message_list = [] result_message_list.append("* PYCODESTYLE: [FAILED]: " + filepath) fchecker.report.print_statistics() for line_number, offset, code, text, doc in fchecker.report._deferred_print: result_message_list.append( _fmt % { 'path': filepath, 'row': fchecker.report.line_offset + line_number, 'col': offset + 1, 'code': code, 'text': text, }) publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list return 1 else: if args.verbose: message = "* PYCODESTYLE: [PASSED]: " + filepath print_stderr(message) return 0 def validate_yaml_contents(filepath, args): def validate_testdef_yaml(y, args): result_message_list = [] if 'metadata' not in y.keys(): result_message_list.append("* METADATA [FAILED]: " + filepath) result_message_list.append("\tmetadata section missing") publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list exit(1) metadata_dict = y['metadata'] mandatory_keys = set([ 'name', 'format', 'description', 'maintainer', 'os', 'devices']) if not mandatory_keys.issubset(set(metadata_dict.keys())): result_message_list.append("* METADATA [FAILED]: " + filepath) result_message_list.append("\tmandatory keys missing: %s" % mandatory_keys.difference(set(metadata_dict.keys()))) result_message_list.append("\tactual keys present: %s" % metadata_dict.keys()) publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list return 1 for key in mandatory_keys: if len(metadata_dict[key]) == 0: result_message_list.append("* METADATA [FAILED]: " + filepath) result_message_list.append("\t%s has no content" % key) publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list return 1 # check if name has white spaces if metadata_dict['name'].find(" ") > -1: result_message_list.append("* METADATA [FAILED]: " + filepath) result_message_list.append("\t'name' contains whitespace") publish_result(result_message_list, args) args.failed_message_list = args.ailed_message_list + result_message_list return 1 # check 'format' value if metadata_dict['format'] not in ["Lava-Test Test Definition 1.0", "Manual Test Definition 1.0"]: result_message_list.append("* METADATA [FAILED]: " + filepath) result_message_list.append("\t'format' has incorrect value") publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list return 1 result_message_list.append("* METADATA [PASSED]: " + filepath) publish_result(result_message_list, args) return 0 def validate_skipgen_yaml(filepath, args): abi = detect_abi() # Run skipgen on skipgen yaml file to check for output and errors skips = subprocess.check_output( "automated/bin/{}/skipgen {}".format(abi, filepath), shell=True).decode('utf-8').strip() if len(skips.split('\n')) < 1: message = "* SKIPGEN [FAILED]: " + filepath + " - No skips found" publish_result([message], args) args.failed_message_list.append(message) return 1 publish_result(["* SKIPGEN [PASSED]: " + filepath], args) return 0 filecontent = None try: with open(filepath, "r") as f: filecontent = f.read() except FileNotFoundError: publish_result(["* YAMLVALIDCONTENTS [PASSED]: " + filepath + " - deleted"], args) return 0 y = yaml.load(filecontent, Loader=yaml.FullLoader) if 'run' in y.keys(): # test definition yaml file return validate_testdef_yaml(y, args) elif 'skiplist' in y.keys(): # skipgen yaml file return validate_skipgen_yaml(filepath, args) else: publish_result(["* YAMLVALIDCONTENTS [SKIPPED]: " + filepath + " - Unknown yaml type detected"], args) return 0 def validate_yaml(filename, args): filecontent = None try: with open(filename, "r") as f: filecontent = f.read() except FileNotFoundError: publish_result(["* YAMLVALID [PASSED]: " + filename + " - deleted"], args) return 0 try: yaml.load(filecontent, Loader=yaml.FullLoader) if args.verbose: message = "* YAMLVALID: [PASSED]: " + filename print_stderr(message) except yaml.YAMLError: message = "* YAMLVALID: [FAILED]: " + filename result_message_list = [] result_message_list.append(message) result_message_list.append("\n\n") exc_type, exc_value, exc_traceback = sys.exc_info() for line in traceback.format_exception_only(exc_type, exc_value): result_message_list.append(' ' + line) publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list return 1 return 0 def validate_shell(filename, ignore_options): ignore_string = "" if args.shellcheck_ignore is not None: # Exclude types of warnings in the following format: # -e CODE1,CODE2.. ignore_string = "-e %s" % ",".join(args.shellcheck_ignore) if len(ignore_string) < 4: # contains only "-e " ignore_string = "" cmd = 'shellcheck %s' % ignore_string return validate_external(cmd, filename, "SHELLCHECK", args) def validate_php(filename, args): cmd = 'php -l' return validate_external(cmd, filename, "PHPLINT", args) def validate_external(cmd, filename, prefix, args): final_cmd = "%s %s 2>&1" % (cmd, filename) status, output = subprocess.getstatusoutput(final_cmd) if status == 0: message = '* %s: [PASSED]: %s' % (prefix, filename) publish_result([message], args) else: result_message_list = [] result_message_list.append('* %s: [FAILED]: %s' % (prefix, filename)) result_message_list.append('* %s: [OUTPUT]:' % prefix) for line in output.splitlines(): result_message_list.append(' ' + line) publish_result(result_message_list, args) args.failed_message_list = args.failed_message_list + result_message_list return 1 return 0 def validate_file(args, path): if args.verbose: print("Validating file: %s" % path) filetype = magic.from_file(path, mime=True) exitcode = 0 # libmagic takes yaml as 'text/plain', so use file extension here. if path.endswith((".yaml", ".yml")): exitcode = validate_yaml(path, args) if exitcode == 0: # if yaml isn't valid there is no point in checking metadata exitcode = validate_yaml_contents(path, args) elif run_pycodestyle and filetype == "text/x-python": exitcode = pycodestyle_check(path, args) elif filetype == "text/x-php": exitcode = validate_php(path, args) elif path.endswith('.sh') or filetype == "text/x-shellscript": exitcode = validate_shell(path, args) else: publish_result(["* UNKNOWN [SKIPPED]: " + path + " - Unknown file type detected: " + filetype], args) return 0 return exitcode def run_unit_tests(args, filelist=None): exitcode = 0 if filelist is not None: for filename in filelist: tmp_exitcode = validate_file(args, filename) if tmp_exitcode != 0: exitcode = 1 else: for root, dirs, files in os.walk('.'): if not root.startswith("./.git"): for name in files: tmp_exitcode = validate_file( args, root + "/" + name) if tmp_exitcode != 0: exitcode = 1 return exitcode def main(args): exitcode = 0 if args.git_latest: # check if git exists git_status, git_result = subprocess.getstatusoutput( "git diff --name-only HEAD~1") if git_status == 0: filelist = git_result.split() exitcode = run_unit_tests(args, filelist) elif len(args.file_path) > 0: exitcode = run_unit_tests(args, [args.file_path]) else: exitcode = run_unit_tests(args) if not args.verbose: publish_result(None, args) exit(exitcode) if __name__ == '__main__': failed_message_list = [] parser = argparse.ArgumentParser() parser.add_argument("-p", "--pycodestyle-ignore", nargs="*", default=["E501"], help="Space separated list of pycodestyle exclusions", dest="pycodestyle_ignore") parser.add_argument("-s", "--shellcheck-ignore", nargs="*", help="Space separated list of shellcheck exclusions", dest="shellcheck_ignore") parser.add_argument("-g", "--git-latest", action="store_true", default=False, help="If set, the script will try to evaluate files in last git \ commit instead of the whole repository", dest="git_latest") parser.add_argument("-f", "--file-path", default="", help="Path to the file that should be checked", dest="file_path") parser.add_argument("-r", "--result-file", default="build-error.txt", help="Path to the file that contains results in case of failure", dest="result_file") parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Make output more verbose", dest="verbose") args = parser.parse_args() setattr(args, "failed_message_list", failed_message_list) main(args)