Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import sys |
| 4 | import os |
| 5 | import json |
| 6 | import argparse |
| 7 | from subprocess import run, PIPE, check_output, CalledProcessError |
| 8 | |
| 9 | rootdir = os.path.dirname(os.path.realpath(__file__)) + '/../..' |
| 10 | |
| 11 | def crc_from_apigen(revision, filename): |
| 12 | if not revision and not os.path.isfile(filename): |
| 13 | print(f'skipping: {filename}', file=sys.stderr) |
| 14 | return {} |
| 15 | apigen_bin = f'{rootdir}/src/tools/vppapigen/vppapigen.py' |
| 16 | if revision: |
| 17 | apigen = (f'{apigen_bin} --git-revision {revision} --includedir src ' |
| 18 | f'--input {filename} CRC') |
| 19 | else: |
| 20 | apigen = (f'{apigen_bin} --includedir src --input {filename} CRC') |
| 21 | rv = run(apigen.split(), stdout=PIPE, stderr=PIPE) |
| 22 | if rv.returncode == 2: # No such file |
| 23 | print(f'skipping: {revision}:{filename} {rv}', file=sys.stderr) |
| 24 | return {} |
| 25 | if rv.returncode != 0: |
| 26 | print(f'vppapigen failed for {revision}:{filename} with command\n {apigen}\n error: {rv}', |
| 27 | rv.stderr.decode('ascii'), file=sys.stderr) |
| 28 | sys.exit(-2) |
| 29 | |
| 30 | return json.loads(rv.stdout) |
| 31 | |
| 32 | |
| 33 | def dict_compare(d1, d2): |
| 34 | d1_keys = set(d1.keys()) |
| 35 | d2_keys = set(d2.keys()) |
| 36 | intersect_keys = d1_keys.intersection(d2_keys) |
| 37 | added = d1_keys - d2_keys |
| 38 | removed = d2_keys - d1_keys |
| 39 | modified = {o: (d1[o], d2[o]) for o in intersect_keys if d1[o]['crc'] != d2[o]['crc']} |
| 40 | same = set(o for o in intersect_keys if d1[o] == d2[o]) |
| 41 | return added, removed, modified, same |
| 42 | |
| 43 | |
| 44 | def filelist_from_git_ls(): |
| 45 | filelist = [] |
| 46 | git_ls = 'git ls-files *.api' |
| 47 | rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE) |
| 48 | if rv.returncode != 0: |
| 49 | sys.exit(rv.returncode) |
| 50 | |
| 51 | for l in rv.stdout.decode('ascii').split('\n'): |
| 52 | if len(l): |
| 53 | filelist.append(l) |
| 54 | return filelist |
| 55 | |
| 56 | |
| 57 | def is_uncommitted_changes(): |
| 58 | git_status = 'git status --porcelain -uno' |
| 59 | rv = run(git_status.split(), stdout=PIPE, stderr=PIPE) |
| 60 | if rv.returncode != 0: |
| 61 | sys.exit(rv.returncode) |
| 62 | |
| 63 | if len(rv.stdout): |
| 64 | return True |
| 65 | return False |
| 66 | |
| 67 | |
| 68 | def filelist_from_git_grep(filename): |
| 69 | filelist = [] |
| 70 | try: |
| 71 | rv = check_output(f'git grep -e "import .*{filename}" -- *.api', shell=True) |
| 72 | except CalledProcessError as err: |
| 73 | return [] |
| 74 | print('RV', err.returncode) |
| 75 | for l in rv.decode('ascii').split('\n'): |
| 76 | if l: |
| 77 | f, p = l.split(':') |
| 78 | filelist.append(f) |
| 79 | return filelist |
| 80 | |
| 81 | |
| 82 | def filelist_from_patchset(): |
| 83 | filelist = [] |
| 84 | git_cmd = '((git diff HEAD~1.. --name-only;git ls-files -m) | sort -u)' |
| 85 | rv = check_output(git_cmd, shell=True) |
| 86 | for l in rv.decode('ascii').split('\n'): |
| 87 | if len(l) and os.path.splitext(l)[1] == '.api': |
| 88 | filelist.append(l) |
| 89 | |
| 90 | # Check for dependencies (imports) |
| 91 | imported_files = [] |
| 92 | for f in filelist: |
| 93 | imported_files.extend(filelist_from_git_grep(os.path.basename(f))) |
| 94 | |
| 95 | filelist.extend(imported_files) |
| 96 | return set(filelist) |
| 97 | |
| 98 | def is_deprecated(d, k): |
| 99 | if 'options' in d[k] and 'deprecated' in d[k]['options']: |
| 100 | return True |
| 101 | return False |
| 102 | |
| 103 | def is_in_progress(d, k): |
| 104 | try: |
| 105 | if d[k]['options']['status'] == 'in_progress': |
| 106 | return True |
| 107 | except: |
| 108 | return False |
| 109 | |
| 110 | def report(old, new, added, removed, modified, same): |
| 111 | backwards_incompatible = 0 |
| 112 | for k in added: |
| 113 | print(f'added: {k}') |
| 114 | for k in removed: |
| 115 | oldversion = int(old[k]['version']) |
| 116 | if oldversion > 0 and not is_deprecated(old, k) and not is_in_progress(old, k): |
| 117 | backwards_incompatible += 1 |
| 118 | print(f'removed: ** {k}') |
| 119 | else: |
| 120 | print(f'removed: {k}') |
| 121 | for k in modified.keys(): |
| 122 | oldversion = int(old[k]['version']) |
| 123 | newversion = int(new[k]['version']) |
| 124 | if oldversion > 0 and not is_in_progress(old, k): |
| 125 | backwards_incompatible += 1 |
| 126 | print(f'modified: ** {k}') |
| 127 | else: |
| 128 | print(f'modified: {k}') |
| 129 | return backwards_incompatible |
| 130 | |
| 131 | |
| 132 | def main(): |
| 133 | parser = argparse.ArgumentParser(description='VPP CRC checker.') |
| 134 | parser.add_argument('--git-revision', |
| 135 | help='Git revision to compare against') |
| 136 | parser.add_argument('--dump-manifest', action='store_true', |
| 137 | help='Dump CRC for all messages') |
| 138 | parser.add_argument('--check-patchset', action='store_true', |
| 139 | help='Dump CRC for all messages') |
| 140 | parser.add_argument('files', nargs='*') |
| 141 | parser.add_argument('--diff', help='Files to compare (on filesystem)', nargs=2) |
| 142 | |
| 143 | args = parser.parse_args() |
| 144 | |
| 145 | if args.diff and args.files: |
| 146 | parser.print_help() |
| 147 | sys.exit(-1) |
| 148 | |
| 149 | # Diff two files |
| 150 | if args.diff: |
| 151 | oldcrcs = crc_from_apigen(None, args.diff[0]) |
| 152 | newcrcs = crc_from_apigen(None, args.diff[1]) |
| 153 | added, removed, modified, same = dict_compare(newcrcs, oldcrcs) |
| 154 | backwards_incompatible = report(oldcrcs, newcrcs, added, removed, modified, same) |
| 155 | sys.exit(0) |
| 156 | |
| 157 | # Dump CRC for messages in given files / revision |
| 158 | if args.dump_manifest: |
| 159 | files = args.files if args.files else filelist_from_git_ls() |
| 160 | crcs = {} |
| 161 | for f in files: |
| 162 | crcs.update(crc_from_apigen(args.git_revision, f)) |
| 163 | for k, v in crcs.items(): |
| 164 | print(f'{k}: {v}') |
| 165 | sys.exit(0) |
| 166 | |
| 167 | # Find changes between current patchset and given revision (previous) |
| 168 | if args.check_patchset: |
| 169 | if args.git_revision: |
| 170 | print('Argument git-revision ignored', file=sys.stderr) |
| 171 | # Check there are no uncomitted changes |
| 172 | if is_uncommitted_changes(): |
| 173 | print('Please stash or commit changes in workspace', file=sys.stderr) |
| 174 | sys.exit(-1) |
| 175 | files = filelist_from_patchset() |
| 176 | else: |
| 177 | # Find changes between current workspace and revision |
| 178 | # Find changes between a given file and a revision |
| 179 | files = args.files if args.files else filelist_from_git_ls() |
| 180 | |
| 181 | revision = args.git_revision if args.git_revision else 'HEAD~1' |
| 182 | |
| 183 | oldcrcs = {} |
| 184 | newcrcs = {} |
| 185 | for f in files: |
| 186 | newcrcs.update(crc_from_apigen(None, f)) |
| 187 | oldcrcs.update(crc_from_apigen(revision, f)) |
| 188 | |
| 189 | added, removed, modified, same = dict_compare(newcrcs, oldcrcs) |
| 190 | backwards_incompatible = report(oldcrcs, newcrcs, added, removed, modified, same) |
| 191 | |
| 192 | if args.check_patchset: |
| 193 | if backwards_incompatible: |
| 194 | # alert on changing production API |
| 195 | print("crcchecker: Changing production APIs in an incompatible way", file=sys.stderr) |
| 196 | sys.exit(-1) |
| 197 | else: |
| 198 | print('*' * 67) |
| 199 | print('* VPP CHECKAPI SUCCESSFULLY COMPLETED') |
| 200 | print('*' * 67) |
| 201 | |
| 202 | if __name__ == '__main__': |
| 203 | main() |