blob: bac8cbfea774849e8cf8cc1ec144995e8d5fe00f [file] [log] [blame]
#!/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.failed_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)