From 4702650752f8984ef15e340f5ea3d926ab1f23e1 Mon Sep 17 00:00:00 2001 From: Ben Hutchings Date: Tue, 8 Sep 2020 15:42:11 +0100 Subject: Add script to validate lorry files Relates to #1. --- README | 16 +++++ scripts/validate-lorries | 176 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100755 scripts/validate-lorries diff --git a/README b/README index 97edebf..e7d568e 100644 --- a/README +++ b/README @@ -16,3 +16,19 @@ add any additional configuration to this repository. Remember, the Lorry tool is not permitted to manage repositories inside your prefix which is baserock. +Validation +---------- + +Run `scripts/validate-lorries` to check the `.lorry` files. This will +report: + +* Invalid YAML/JSON syntax +* Wrong data types +* Duplicate repository names +* Missing required keys +* Unexpected keys +* Invalid values + +The syntax and duplicate name checks are applied even to disabled +`.lorry` files, because reusing a repository name for a different +upstream type generally won't work. diff --git a/scripts/validate-lorries b/scripts/validate-lorries new file mode 100755 index 0000000..0d1d880 --- /dev/null +++ b/scripts/validate-lorries @@ -0,0 +1,176 @@ +#!/usr/bin/python3 + +import glob +import json +import sys +import yaml + + +def check_string(v): + if not isinstance(v, str): + return 'must be a string' + + +def check_bool(v): + if not isinstance(v, bool): + return 'must be a boolean' + + +def check_type_string(v): + err = check_string(v) + if err is not None: + return err + + if v not in ['bzr', 'cvs', 'git', 'gzip', 'hg', 'svn', 'tarball', 'zip']: + return '"%s" is not a recognised type' % v + + +def check_stringlist(v): + if not isinstance(v, list): + return 'must be a list' + + for elem in v: + if not isinstance(elem, str): + return 'must have strings as elements' + + +def check_stringdict(v): + if not isinstance(v, dict): + return 'must be a dictionary' + + for key, value in v.items(): + if not (isinstance(key, str) and isinstance(value, str)): + return 'must have strings as keys and value' + + +def check_svn_layout(v): + if v == 'standard': + return + + if not isinstance(v, dict): + return 'must be either "standard" or a dictionary' + + return check_stringdict(v) + + +def validate(filename, repo_filenames, strict=True): + is_ok = True + repo_name = None + + def diagnostic(level, msg): + if repo_name is None: + print('%s: %s: %s' % (level, filename, msg), + file=sys.stderr) + else: + print('%s: %s: %s: %s' % (level, filename, repo_name, msg), + file=sys.stderr) + + def error(msg): + nonlocal is_ok + is_ok = False + diagnostic('E', msg) + + def warning(msg): + diagnostic('W', msg) + + with open(filename) as f: + try: + try: + obj = yaml.safe_load(f) + except yaml.YAMLError: + f.seek(0) + obj = json.load(f) + except ValueError: + error('not valid YAML or JSON') + return is_ok + + if not isinstance(obj, dict): + error('must be a dictionary') + return is_ok + + for repo_name, upstream_def in obj.items(): + if repo_name in repo_filenames: + error('repository already defined in %s' + % repo_filenames[repo_name]) + else: + repo_filenames[repo_name] = filename + if not strict: + continue + + upstream_type = upstream_def.get('type') + + value_checkers = { + # Keys listed in Lorry's README + 'type': check_type_string, + 'url': check_string, + 'check-certificates': check_bool, + 'branches': check_stringdict, + 'layout': check_svn_layout, + 'module': check_string, + # Undocumented Lorry feature + 'refspecs': check_stringlist, + # Lorry Controller extension + 'description': check_string, + } + + required_keys = set(['type']) + optional_keys = set(['refspecs', 'description']) + if upstream_type != 'bzr': + required_keys.add('url') + else: + optional_keys.add('url') + optional_keys.add('branches') + if upstream_type in ['bzr', 'git', 'hg']: + optional_keys.add('check-certificates') + if upstream_type == 'svn': + required_keys.add('layout') + if upstream_type == 'cvs': + required_keys.add('module') + + for key in required_keys: + if key not in upstream_def: + error('missing "%s" key' % key) + + # For bzr, exactly one of url and branches keys is required + if upstream_type == 'bzr': + has_url = 'url' in upstream_def + has_branches = 'branches' in upstream_def + if has_url and has_branches: + error('has both "url" and "branches" keys') + elif not has_url and not has_branches: + error('missing both "url" and "branches" keys') + + for key, value in upstream_def.items(): + if key.startswith('x-products-'): + # Baserock Import extension + msg = check_stringlist(value) + else: + if key not in required_keys and key not in optional_keys: + warning('unexpected "%s" key' % key) + if key in value_checkers: + msg = value_checkers[key](value) + else: + msg = None + if msg: + error('%s: %s' % (key, msg)) + + return is_ok + + +def main(): + repo_filenames = {} + all_ok = True + + for filename in glob.glob('*-lorries/*.lorry'): + if not validate(filename, repo_filenames): + all_ok = False + + for filename in glob.glob('*-lorries-disabled/*.lorry'): + if not validate(filename, repo_filenames, strict=False): + all_ok = False + + sys.exit(0 if all_ok else 1) + + +if __name__ == '__main__': + main() -- cgit v1.2.1