Commit 5a0eea65 authored by Luke Mewburn's avatar Luke Mewburn
Browse files

asn_process: implement backwards compat checking

Use compat_asn1 to check backwards Compatibility after linting.

Detect if a compat exception is configured but not raised
(e.g, an issue is fixed but the config isn't updated.)

Add new per-asn_file key to asn_config.json: compat_files,
which is an object whose keys are files to check backwards
compatibility with, and values are a list of error messages.

Add asn_config.json entries for each file against its previous release.
Currently 33128/r17/TS33128Payloads has various compatibility issues
with r18 (and r19).
parent cbe4fa3e
Loading
Loading
Loading
Loading
+91 −3
Original line number Diff line number Diff line
@@ -11,6 +11,17 @@
    },
    "33128/r16/TS33128IdentityAssociation.asn": {},
    "33128/r16/TS33128Payloads.asn": {
      "compat_files": {
        "33128/r15/TS33128Payloads.asn": [
          "TS33128Payloads.XIRIPayload.relativeOID[1] renamed to 'xIRIPayloadOID[1]'",
          "TS33128Payloads.GlobalRANNodeID.aNNodeID[2] type 'CHOICE' changed to 'TS33128Payloads.ANNodeID'",
          "TS33128Payloads.SMSMessage.transferStatus[4] renamed to 'linkTransferStatus[4]'",
          "TS33128Payloads.LALSReport missing field 'pEI[2]'",
          "TS33128Payloads.IRIPayload.relativeOID[1] renamed to 'iRIPayloadOID[1]'",
          "TS33128Payloads.CCPayload.relativeOID[1] renamed to 'cCPayloadOID[1]'",
          "TS33128Payloads.LINotificationPayload.relativeOID[1] renamed to 'lINotificationPayloadOID[1]'"
        ]
      },
      "lint_exceptions": [
        "Tag 16 IRIEvent field 'mDFCellSiteReport' is not present in XIRIEvent",
        "Tag 5 XIRIEvent field 'unsuccessfulAMProcedure' differs from IRIEvent field 'unsuccessfulRegistrationProcedure'",
@@ -28,8 +39,32 @@
        "Tags 65-164 missing in IRIEvent"
      ]
    },
    "33128/r17/TS33128IdentityAssociation.asn": {},
    "33128/r17/TS33128IdentityAssociation.asn": {
      "compat_files": {
        "33128/r16/TS33128IdentityAssociation.asn": []
      }
    },
    "33128/r17/TS33128Payloads.asn": {
      "compat_files": {
        "33128/r16/TS33128Payloads.asn": [
          "TS33128Payloads.EUTRALocation.ageOfLocatonInfo[3] renamed to 'ageOfLocationInfo[3]'",
          "TS33128Payloads.NRLocation.ageOfLocatonInfo[3] renamed to 'ageOfLocationInfo[3]'",
          "TS33128Payloads.HorizontalWithVerticalVelocityAndUncertainty.hspeed[1] renamed to 'hSpeed[1]'",
          "TS33128Payloads.PDSummaryReport new field 'perSessionTrigger[14]' is not OPTIONAL",
          "TS33128Payloads.XIRIEvent.aMFIdentifierAssocation[62] renamed to 'aMFIdentifierAssociation[62]'",
          "TS33128Payloads.XIRIEvent.aMFIdentifierAssocation[62] type 'TS33128Payloads.AMFIdentifierAssocation' changed to 'TS33128Payloads.AMFIdentifierAssociation'",
          "TS33128Payloads.XIRIEvent.mMEIdentifierAssocation[63] renamed to 'mMEIdentifierAssociation[63]'",
          "TS33128Payloads.XIRIEvent.mMEIdentifierAssocation[63] type 'TS33128Payloads.MMEIdentifierAssocation' changed to 'TS33128Payloads.MMEIdentifierAssociation'",
          "TS33128Payloads.IRIEvent.aMFIdentifierAssocation[62] renamed to 'aMFIdentifierAssociation[62]'",
          "TS33128Payloads.IRIEvent.aMFIdentifierAssocation[62] type 'TS33128Payloads.AMFIdentifierAssocation' changed to 'TS33128Payloads.AMFIdentifierAssociation'",
          "TS33128Payloads.IRIEvent.mMEIdentifierAssocation[63] renamed to 'mMEIdentifierAssociation[63]'",
          "TS33128Payloads.IRIEvent.mMEIdentifierAssocation[63] type 'TS33128Payloads.MMEIdentifierAssocation' changed to 'TS33128Payloads.MMEIdentifierAssociation'",
          "TS33128Payloads.CCPDU.pTCCCPDU[4] renamed to 'nIDDCCPDU[4]'",
          "TS33128Payloads.CCPDU.pTCCCPDU[4] type 'TS33128Payloads.PTCCCPDU' changed to 'TS33128Payloads.NIDDCCPDU'",
          "Type AMFIdentifierAssocation not present",
          "Type MMEIdentifierAssocation not present"
        ]
      },
      "lint_exceptions": [
        "Tag 100 XIRIEvent field 'n9HRPDUSessionInfo' is not present in IRIEvent",
        "Tag 101 XIRIEvent field 's8HRBearerInfo' is not present in IRIEvent",
@@ -60,8 +95,49 @@
        "Tags 167-185 missing in IRIEvent"
      ]
    },
    "33128/r18/TS33128IdentityAssociation.asn": {},
    "33128/r18/TS33128IdentityAssociation.asn": {
      "compat_files": {
        "33128/r17/TS33128IdentityAssociation.asn": []
      }
    },
    "33128/r18/TS33128Payloads.asn": {
      "compat_files": {
        "33128/r17/TS33128Payloads.asn": [
          "TS33128Payloads.EUTRALocation.ageOfLocationInfo[3] type 'INTEGER' changed to 'TS33128Payloads.AgeOfLocation'",
          "TS33128Payloads.NRLocation.ageOfLocationInfo[3] type 'INTEGER' changed to 'TS33128Payloads.AgeOfLocation'",
          "TS33128Payloads.N3GALocation.ageOfLocationInfo[11] type 'INTEGER' changed to 'TS33128Payloads.AgeOfLocation'",
          "TS33128Payloads.PointUncertaintyCircle.uncertainty[2] renamed to 'deprecatedUncertainty[2]'",
          "TS33128Payloads.UncertaintyEllipse.semiMajor[1] renamed to 'deprecatedSemiMajor[1]'",
          "TS33128Payloads.UncertaintyEllipse.semiMinor[2] renamed to 'deprecatedSemiMinor[2]'",
          "TS33128Payloads.PointAltitudeUncertainty.uncertaintyAltitude[4] renamed to 'deprecatedUncertaintyAltitude[4]'",
          "TS33128Payloads.EllipsoidArc.uncertaintyRadius[3] renamed to 'deprecatedUncertaintyRadius[3]'",
          "TS33128Payloads.LocationData.ageOfLocationEstimate[3] type 'TS33128Payloads.AgeOfLocationEstimate' changed to 'TS33128Payloads.AgeOfLocation'",
          "TS33128Payloads.AMFRegistration.additionalUserIdentifiers[19] renamed to 'rRCEstablishmentCause[19]'",
          "TS33128Payloads.AMFRegistration.additionalUserIdentifiers[19] type 'TS33128Payloads.UserIdentifiers' changed to 'TS33128Payloads.RRCEstablishmentCause'",
          "TS33128Payloads.UserIdentifiers type 'SEQUENCE' mismatch with TS33128Payloads.RRCEstablishmentCause 'CHOICE'",
          "TS33128Payloads.AMFDeregistration.additionalUserIdentifiers[12] renamed to 'unavailabilityPeriodDuration[12]'",
          "TS33128Payloads.AMFDeregistration.additionalUserIdentifiers[12] type 'TS33128Payloads.UserIdentifiers' changed to 'TS33128Payloads.UnavailabilityPeriodDuration'",
          "TS33128Payloads.UserIdentifiers type 'SEQUENCE' mismatch with TS33128Payloads.UnavailabilityPeriodDuration 'OCTET STRING'",
          "TS33128Payloads.AMFLocationUpdate.sMSOverNASIndicator[7] renamed to 'deprecatedSMSOverNASIndicator[7]'",
          "TS33128Payloads.AMFLocationUpdate.oldGUTI[8] renamed to 'deprecatedOldGUTI[8]'",
          "TS33128Payloads.AMFLocationUpdate.additionalUserIdentifiers[9] renamed to 'uEAreaIndication[9]'",
          "TS33128Payloads.AMFLocationUpdate.additionalUserIdentifiers[9] type 'TS33128Payloads.UserIdentifiers' changed to 'TS33128Payloads.UEAreaIndication'",
          "TS33128Payloads.UserIdentifiers.fiveGSSubscriberIDs[1] renamed to 'country[1]'",
          "TS33128Payloads.UserIdentifiers.fiveGSSubscriberIDs[1] type 'TS33128Payloads.FiveGSSubscriberIDs' changed to 'UTF8String'",
          "TS33128Payloads.FiveGSSubscriberIDs type 'SEQUENCE' mismatch with TS33128Payloads.UEAreaIndication.country[1] 'UTF8String'",
          "TS33128Payloads.UserIdentifiers.ePSSubscriberIDs[2] renamed to 'internationalAreaIndication[2]'",
          "TS33128Payloads.UserIdentifiers.ePSSubscriberIDs[2] type 'TS33128Payloads.EPSSubscriberIDs' changed to 'BOOLEAN'",
          "TS33128Payloads.EPSSubscriberIDs type 'SEQUENCE' mismatch with TS33128Payloads.UEAreaIndication.internationalAreaIndication[2] 'BOOLEAN'",
          "TS33128Payloads.AMFStartOfInterceptionWithRegisteredUE.additionalUserIdentifiers[16] renamed to 'sORTransparentContainer[16]'",
          "TS33128Payloads.AMFStartOfInterceptionWithRegisteredUE.additionalUserIdentifiers[16] type 'TS33128Payloads.UserIdentifiers' changed to 'TS33128Payloads.SORTransparentContainer'",
          "TS33128Payloads.UserIdentifiers type 'SEQUENCE' mismatch with TS33128Payloads.SORTransparentContainer 'OCTET STRING'",
          "TS33128Payloads.PDSummaryReport.perSessionTrigger[14] renamed to 'useSessionTrigger[14]'",
          "TS33128Payloads.IRIEvent.unsuccessfulRegistrationProcedure[5] renamed to 'unsuccessfulAMProcedure[5]'",
          "TS33128Payloads.IRIEvent.unsuccessfulSessionProcedure[10] renamed to 'unsuccessfulSMProcedure[10]'",
          "TS33128Payloads.IRIEvent.uDMLocationInformationResultRecord[108] renamed to 'uDMLocationInformationResult[108]'",
          "Type AgeOfLocationEstimate not present"
        ]
      },
      "dependencies": [
        "testing/dependencies/asn/IPAccessPDU.asn",
        "testing/dependencies/asn/stubs/LI-PS-PDU.asn"
@@ -94,8 +170,20 @@
        "Tag 23 missing in SMFPDUSessionModification"
      ]
    },
    "33128/r19/TS33128IdentityAssociation.asn": {},
    "33128/r19/TS33128IdentityAssociation.asn": {
      "compat_files": {
        "33128/r18/TS33128IdentityAssociation.asn": []
      }
    },
    "33128/r19/TS33128Payloads.asn": {
      "compat_files": {
        "33128/r18/TS33128Payloads.asn": [
          "TS33128Payloads.AMFRegistration.nGInformation[20] renamed to 'deprecatedNGInformation[20]'",
          "TS33128Payloads.MMEAttach.s1Information[15] renamed to 'deprecatedS1Information[15]'",
          "TS33128Payloads.MMEStartOfInterceptionWithEPSAttachedUE.s1Information[16] renamed to 'deprecatedS1Information[16]'",
          "TS33128Payloads.StartOfInterceptionWithEstablishedIMSDataChannel.mediaInstructionData[5] renamed to 'mediaInstructions[5]'"
        ]
      },
      "dependencies": [
        "testing/dependencies/asn/IPAccessPDU.asn",
        "testing/dependencies/asn/stubs/LI-PS-PDU.asn"
+92 −12
Original line number Diff line number Diff line
@@ -5,10 +5,12 @@ import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

import pycrate_asn1c.asnproc

import compat_asn1
import lint_asn1


@@ -52,6 +54,18 @@ def fix_path(path):
        return path


def getDependencies(asnFiles, fileName):
    """Return list of dependencies for fileName, and text of all files.

    :param asnFiles: dict; key is filename, value is dict with optional keys:
      dependencies: list of dependencies.
    :returns: A list of dependencies with fileName first, and the text of all files.
    """
    fileNames = [fileName]
    fileNames.extend(asnFiles.get(fileName, {}).get("dependencies", []))
    return fileNames


def syntaxCheckASN(fileList):
    """
    Performs ASN syntax checking on a list of filenames (or pathlib Paths)
@@ -95,10 +109,11 @@ def syntaxCheckASN(fileList):

def compileAllTargets(asnFiles):
    """
    Attempts to compile a set of compile targets using the pycrate ASN1 tools
    Attempts to compile a set of compile targets using the pycrate ASN.1 tools

    :param asnFiles: dict; key is filename, value is dict with optional keys:
      dependencies, list of dependencies.
      compat_files:
      dependencies: list of dependencies.
    :returns: A dict of outcome against the first filename of each compile target. Return code and message are included for failures.

    For each compile target (list of filenames) the first filename is assumed
@@ -117,8 +132,7 @@ def compileAllTargets(asnFiles):
        logging.debug(f"Compiling {target}")
        try:
            fileTexts = []
            fileNames = [target]
            fileNames.extend(config.get("dependencies", []))
            fileNames = getDependencies(asnFiles, target)
            pycrate_asn1c.asnproc.GLOBAL.clear()
            for filename in fileNames:
                pFile = Path(filename)
@@ -134,6 +148,59 @@ def compileAllTargets(asnFiles):
    return results


def compatCheck(asnFiles):
    """
    Attempts to check backwards compatibility using the pycrate ASN.1 tools.

    :param asnFiles: dict; key is filename, value is dict with optional keys:
      dependencies: list of dependencies.
      compat_files: dict of filenames to check, value is list of expected errors.
    :returns: A dict of outcome against the first filename of each compile target. Return code and message are included for failures.

    For each file in asnFiles:
      For each compat in file['compat_files']
         check file against compat, and the errors should match the value.

    """
    results = {}
    for target, config in asnFiles.items():
        logging.info(f"Compatibility checking {target}")
        try:
            errors = []
            compatFiles = config.get("compat_files", {})
            if len(compatFiles) > 0:
                logging.info(f"Check {target} against {' '.join(compatFiles.keys())}")
                targetDeps = getDependencies(asnFiles, target)
                for compatName, compatExceptions in compatFiles.items():
                    compatDeps = getDependencies(asnFiles, compatName)
                    ctx = compat_asn1.Context()
                    compat_asn1.compare_files(
                        ctx=ctx, a_files=compatDeps, b_files=targetDeps
                    )
                    errors.extend(
                        [
                            {"message": error}
                            for error in ctx.errors
                            if error not in compatExceptions
                        ]
                    )
                    errors.extend(
                        [
                            {"message": f"Missing error: {error}"}
                            for error in compatExceptions
                            if error not in ctx.errors
                        ]
                    )
            results[str(target)] = {
                "ok": len(errors) == 0,
                "message": errors,
            }
        except Exception as ex:
            results[str(target)] = {"ok": False, "code": -1, "message": f"{ex!r}"}
            continue
    return results


def processResults(results, stageName):
    """
    Counts the number of errors and writes out the output per filename
@@ -165,12 +232,13 @@ def processResults(results, stageName):
    return errorCount


if __name__ == "__main__":
def main():
    loglevel = os.environ.get("LOGLEVEL", "WARNING").upper()
    logging.basicConfig(level=loglevel)
    pycrate_asn1c.utils.logger.setLevel(loglevel)

    logging.info("Searching for ASN1C")
    global asn1c_path
    asn1c_path = shutil.which("asn1c")
    if asn1c_path is None:
        raise Exception("No asn1c executable found. Please install asn1c")
@@ -179,6 +247,7 @@ if __name__ == "__main__":
        logging.info(
            "asn1c is a batch file, so assume path separators need to be changed"
        )
        global change_path_to_unix
        change_path_to_unix = True

    logging.info("Loading asn_config.json")
@@ -197,20 +266,22 @@ if __name__ == "__main__":

    if len(parseFileList) == 0:
        logging.warning("No files specified")
        exit(0)
        return 1

    logging.info("Parsing ASN1 files")
    logging.info("Parsing ASN.1 files")
    parseResults = syntaxCheckASN(parseFileList)
    if processResults(parseResults, "Parsing") > 0:
        exit(1)
        return 1

    logging.info("Getting compile targets")
    logging.info("Compiling ASN.1 files")
    asnFiles = asnConfig["asn_files"]
    logging.info(f"{len(asnFiles)} compile targets found")

    compileResults = compileAllTargets(asnFiles)
    if processResults(compileResults, "Compiling") > 0:
        exit(1)
        return 1

    retval = 0  # Checks now don't prevent subsequent checks from running

    logging.info("Linting files")
    lintFileList = filterIgnoredFiles(fileList, asnConfig["lint_ignore"])
@@ -219,6 +290,15 @@ if __name__ == "__main__":
        logging.debug(f"  {file}")
    lintResults = lint_asn1.lintASN1Files(lintFileList, asnConfig)
    if processResults(lintResults, "Linting") > 0:
        exit(1)
        retval = 1

    exit(0)
    logging.info("Checking ASN.1 backwards compatibility")
    compatResults = compatCheck(asnFiles)
    if processResults(compatResults, "Compatibility checking") > 0:
        retval = 1

    return retval


if __name__ == "__main__":
    sys.exit(main())