summaryrefslogtreecommitdiffstats
path: root/src/vnet/cop
diff options
context:
space:
mode:
authorKingwel Xie <kingwel.xie@ericsson.com>2018-10-26 23:42:15 -0400
committerDamjan Marion <dmarion@me.com>2018-10-27 09:14:00 +0000
commit4af47092eec97ce88f66174570cfc6a7baad4c6c (patch)
treeaba1f35887db4a14a5f7c91440aa84acc7ed9dc7 /src/vnet/cop
parent6e43e0680fc21c6a986289cec4406c3624d3bbe6 (diff)
pg: allow creating pg for udp based protocols
f.g., gtpu4/6 Change-Id: I8bb1dc5fd2fba89ff17ec069a9816bafb9684190 Signed-off-by: Kingwel Xie <kingwel.xie@ericsson.com>
Diffstat (limited to 'src/vnet/cop')
0 files changed, 0 insertions, 0 deletions
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
#!/usr/bin/env python3

'''
crcchecker is a tool to used to enforce that .api messages do not change.
API files with a semantic version < 1.0.0 are ignored.
'''

import sys
import os
import json
import argparse
import re
from subprocess import run, PIPE, check_output, CalledProcessError

# pylint: disable=subprocess-run-check

ROOTDIR = os.path.dirname(os.path.realpath(__file__)) + '/../..'
APIGENBIN = f'{ROOTDIR}/src/tools/vppapigen/vppapigen.py'


def crc_from_apigen(revision, filename):
    '''Runs vppapigen with crc plugin returning a JSON object with CRCs for
    all APIs in filename'''
    if not revision and not os.path.isfile(filename):
        print(f'skipping: {filename}', file=sys.stderr)
        # Return <class 'set'> instead of <class 'dict'>
        return {-1}

    if revision:
        apigen = (f'{APIGENBIN} --git-revision {revision} --includedir src '
                  f'--input {filename} CRC')
    else:
        apigen = (f'{APIGENBIN} --includedir src --input {filename} CRC')
    returncode = run(apigen.split(), stdout=PIPE, stderr=PIPE)
    if returncode.returncode == 2:  # No such file
        print(f'skipping: {revision}:{filename} {returncode}', file=sys.stderr)
        return {}
    if returncode.returncode != 0:
        print(f'vppapigen failed for {revision}:{filename} with '
              'command\n {apigen}\n error: {rv}',
              returncode.stderr.decode('ascii'), file=sys.stderr)
        sys.exit(-2)

    return json.loads(returncode.stdout)


def dict_compare(dict1, dict2):
    '''Compare two dictionaries returning added, removed, modified
    and equal entries'''
    d1_keys = set(dict1.keys())
    d2_keys = set(dict2.keys())
    intersect_keys = d1_keys.intersection(d2_keys)
    added = d1_keys - d2_keys
    removed = d2_keys - d1_keys
    modified = {o: (dict1[o], dict2[o]) for o in intersect_keys
                if dict1[o]['crc'] != dict2[o]['crc']}
    same = set(o for o in intersect_keys if dict1[o] == dict2[o])
    return added, removed, modified, same


def filelist_from_git_ls():
    '''Returns a list of all api files in the git repository'''
    filelist = []
    git_ls = 'git ls-files *.api'
    returncode = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
    if returncode.returncode != 0:
        sys.exit(returncode.returncode)

    for line in returncode.stdout.decode('ascii').split('\n'):
        if line:
            filelist.append(line)
    return filelist


def is_uncommitted_changes():
    '''Returns true if there are uncommitted changes in the repo'''
    git_status = 'git status --porcelain -uno'
    returncode = run(git_status.split(), stdout=PIPE, stderr=PIPE)
    if returncode.returncode != 0:
        sys.exit(returncode.returncode)

    if returncode.stdout:
        return True
    return False


def filelist_from_git_grep(filename):
    '''Returns a list of api files that this <filename> api files imports.'''
    filelist = []
    try:
        returncode = check_output(f'git grep -e "import .*{filename}"'
                                  ' -- *.api',
                                  shell=True)
    except CalledProcessError:
        return []
    for line in returncode.decode('ascii').split('\n'):
        if line:
            filename, _ = line.split(':')
            filelist.append(filename)
    return filelist


def filelist_from_patchset(pattern):
    '''Returns list of api files in changeset and the list of api
    files they import.'''
    filelist = []
    git_cmd = ('((git diff HEAD~1.. --name-only;git ls-files -m) | '
               'sort -u | grep "\\.api$")')
    try:
        res = check_output(git_cmd, shell=True)
    except CalledProcessError:
        return []

    # Check for dependencies (imports)
    imported_files = []
    for line in res.decode('ascii').split('\n'):
        if not line:
            continue
        if not re.search(pattern, line):
            continue
        filelist.append(line)
        imported_files.extend(filelist_from_git_grep(os.path.basename(line)))

    filelist.extend(imported_files)
    return set(filelist)


def is_deprecated(message):
    '''Given a message, return True if message is deprecated'''
    if 'options' in message:
        if 'deprecated' in message['options']:
            return True
        # recognize the deprecated format
        if 'status' in message['options'] and \
           message['options']['status'] == 'deprecated':
            print("WARNING: please use 'option deprecated;'")
            return True
    return False


def is_in_progress(message):
    '''Given a message, return True if message is marked as in_progress'''
    if 'options' in message:
        if 'in_progress' in message['options']:
            return True
        # recognize the deprecated format
        if 'status' in message['options'] and \
           message['options']['status'] == 'in_progress':
            print("WARNING: please use 'option in_progress;'")
            return True
    return False


def report(new, old):
    '''Given a dictionary of new crcs and old crcs, print all the
    added, removed, modified, in-progress, deprecated messages.
    Return the number of backwards incompatible changes made.'''

    # pylint: disable=too-many-branches

    new.pop('_version', None)
    old.pop('_version', None)
    added, removed, modified, _ = dict_compare(new, old)
    backwards_incompatible = 0

    # print the full list of in-progress messages
    # they should eventually either disappear of become supported
    for k in new.keys():
        newversion = int(new[k]['version'])
        if newversion == 0 or is_in_progress(new[k]):
            print(f'in-progress: {k}')
    for k in added:
        print(f'added: {k}')
    for k in removed:
        oldversion = int(old[k]['version'])
        if oldversion > 0 and not is_deprecated(old[k]) and not \
           is_in_progress(old[k]):
            backwards_incompatible += 1
            print(f'removed: ** {k}')
        else:
            print(f'removed: {k}')
    for k in modified.keys():
        oldversion = int(old[k]['version'])
        newversion = int(new[k]['version'])
        if oldversion > 0 and not is_in_progress(old[k]):
            backwards_incompatible += 1
            print(f'modified: ** {k}')
        else:
            print(f'modified: {k}')

    # check which messages are still there but were marked for deprecation
    for k in new.keys():
        newversion = int(new[k]['version'])
        if newversion > 0 and is_deprecated(new[k]):
            if k in old:
                if not is_deprecated(old[k]):
                    print(f'deprecated: {k}')
            else:
                print(f'added+deprecated: {k}')

    return backwards_incompatible


def check_patchset():
    '''Compare the changes to API messages in this changeset.
    Ignores API files with version < 1.0.0.
    Only considers API files located under the src directory in the repo.
    '''
    files = filelist_from_patchset('^src/')
    revision = 'HEAD~1'

    oldcrcs = {}
    newcrcs = {}
    for filename in files:
        # Ignore files that have version < 1.0.0
        _ = crc_from_apigen(None, filename)
        # Ignore removed files
        if isinstance(_, set) == 0:
            if isinstance(_, set) == 0 and _['_version']['major'] == '0':
                continue
            newcrcs.update(_)

        oldcrcs.update(crc_from_apigen(revision, filename))

    backwards_incompatible = report(newcrcs, oldcrcs)
    if backwards_incompatible:
        # alert on changing production API
        print("crcchecker: Changing production APIs in an incompatible way",
              file=sys.stderr)
        sys.exit(-1)
    else:
        print('*' * 67)
        print('* VPP CHECKAPI SUCCESSFULLY COMPLETED')
        print('*' * 67)


def main():
    '''Main entry point.'''
    parser = argparse.ArgumentParser(description='VPP CRC checker.')
    parser.add_argument('--git-revision',
                        help='Git revision to compare against')
    parser.add_argument('--dump-manifest', action='store_true',
                        help='Dump CRC for all messages')
    parser.add_argument('--check-patchset', action='store_true',
                        help='Check patchset for backwards incompatbile changes')
    parser.add_argument('files', nargs='*')
    parser.add_argument('--diff', help='Files to compare (on filesystem)',
                        nargs=2)

    args = parser.parse_args()

    if args.diff and args.files:
        parser.print_help()
        sys.exit(-1)

    # Diff two files
    if args.diff:
        oldcrcs = crc_from_apigen(None, args.diff[0])
        newcrcs = crc_from_apigen(None, args.diff[1])
        backwards_incompatible = report(newcrcs, oldcrcs)
        sys.exit(0)

    # Dump CRC for messages in given files / revision
    if args.dump_manifest:
        files = args.files if args.files else filelist_from_git_ls()
        crcs = {}
        for filename in files:
            crcs.update(crc_from_apigen(args.git_revision, filename))
        for k, value in crcs.items():
            print(f'{k}: {value}')
        sys.exit(0)

    # Find changes between current patchset and given revision (previous)
    if args.check_patchset:
        if args.git_revision:
            print('Argument git-revision ignored', file=sys.stderr)
        # Check there are no uncomitted changes
        if is_uncommitted_changes():
            print('Please stash or commit changes in workspace',
                  file=sys.stderr)
            sys.exit(-1)
        check_patchset()
        sys.exit(0)

    # Find changes between current workspace and revision
    # Find changes between a given file and a revision
    files = args.files if args.files else filelist_from_git_ls()

    revision = args.git_revision if args.git_revision else 'HEAD~1'

    oldcrcs = {}
    newcrcs = {}
    for file in files:
        newcrcs.update(crc_from_apigen(None, file))
        oldcrcs.update(crc_from_apigen(revision, file))

    backwards_incompatible = report(newcrcs, oldcrcs)

    if args.check_patchset:
        if backwards_incompatible:
            # alert on changing production API
            print("crcchecker: Changing production APIs in an incompatible way", file=sys.stderr)
            sys.exit(-1)
        else:
            print('*' * 67)
            print('* VPP CHECKAPI SUCCESSFULLY COMPLETED')
            print('*' * 67)


if __name__ == '__main__':
    main()