Add the option to check application state

After using one of the state changing functions you can have it check
the state

    - Add appIDs method
    - Add method to check that all app id's are consistent and unique
diff --git a/TestON/drivers/common/cli/onosclidriver.py b/TestON/drivers/common/cli/onosclidriver.py
index d971471..a997933 100644
--- a/TestON/drivers/common/cli/onosclidriver.py
+++ b/TestON/drivers/common/cli/onosclidriver.py
@@ -21,6 +21,7 @@
 import re
 import json
 import types
+import time
 sys.path.append( "../" )
 from drivers.common.clidriver import CLI
 
@@ -2396,38 +2397,35 @@
         Interacts with the app command for ONOS. This command manages
         application inventory.
         """
-        # Validate argument types
-        valid = True
-        if not isinstance( appName, types.StringType ):
-            main.log.error( self.name + ".app(): appName must be a string" )
-            valid = False
-        if not isinstance( option, types.StringType ):
-            main.log.error( self.name + ".app(): option must be a string" )
-            valid = False
-        if not valid:
-            return main.FALSE
-        # Validate Option
-        option = option.lower()
-        # NOTE: Install may become a valid option
-        if option == "activate":
-            pass
-        elif option == "deactivate":
-            pass
-        elif option == "uninstall":
-            pass
-        else:
-            # Invalid option
-            main.log.error( "The ONOS app command argument only takes the " +
-                            "values: (activate|deactivate|uninstall); was " +
-                            "given '" + option + "'")
-            return main.FALSE
         try:
+            # Validate argument types
+            valid = True
+            if not isinstance( appName, types.StringType ):
+                main.log.error( self.name + ".app(): appName must be a " +
+                                "string" )
+                valid = False
+            if not isinstance( option, types.StringType ):
+                main.log.error( self.name + ".app(): option must be a string" )
+                valid = False
+            if not valid:
+                return main.FALSE
+            # Validate Option
+            option = option.lower()
+            # NOTE: Install may become a valid option
+            if option == "activate":
+                pass
+            elif option == "deactivate":
+                pass
+            elif option == "uninstall":
+                pass
+            else:
+                # Invalid option
+                main.log.error( "The ONOS app command argument only takes " +
+                                "the values: (activate|deactivate|uninstall)" +
+                                "; was given '" + option + "'")
+                return main.FALSE
             cmdStr = "onos:app " + option + " " + appName
             output = self.sendline( cmdStr )
-            # FIXME: look at specific exceptions/Errors
-            # Some Possible outputs:
-            # app a b -> No such application:
-            # app a -> Error executing command onos:app: argument name is required
             if "Error executing command" in output:
                 main.log.error( "Error in processing onos:app command: " +
                                 str( output ) )
@@ -2440,9 +2438,12 @@
                 main.log.error( "Error in processing onos:app command: " +
                                 str( output ) )
                 return main.FALSE
-
+            elif "Unsupported command:" in output:
+                main.log.error( "Incorrect command given to 'app': " +
+                                str( output ) )
             # NOTE: we may need to add more checks here
-            main.log.debug( "app response: " + str( output ) )
+            # else: Command was successful
+            main.log.debug( "app response: " + repr( output ) )
             return main.TRUE
         except TypeError:
             main.log.exception( self.name + ": Object not as expected" )
@@ -2457,9 +2458,12 @@
             main.cleanup()
             main.exit()
 
-    def activateApp( self, appName ):
+    def activateApp( self, appName, check=True ):
         """
         Activate an app that is already installed in ONOS
+        appName is the hierarchical app name, not the feature name
+        If check is True, method will check the status of the app after the
+        command is issued
         Returns main.TRUE if the command was successfully sent
                 main.FALSE if the cli responded with an error or given
                     incorrect input
@@ -2472,7 +2476,17 @@
             status = self.appStatus( appName )
             if status == "INSTALLED":
                 response = self.app( appName, "activate" )
-                return response
+                if check and response == main.TRUE:
+                    for i in range(10):  # try 10 times then give up
+                        # TODO: Check with Thomas about this delay
+                        status = self.appStatus( appName )
+                        if status == "ACTIVE":
+                            return main.TRUE
+                        else:
+                            time.sleep( 1 )
+                    return main.FALSE
+                else:  # not 'check' or command didn't succeed
+                    return response
             elif status == "ACTIVE":
                 return main.TRUE
             elif status == "UNINSTALLED":
@@ -2496,9 +2510,12 @@
             main.cleanup()
             main.exit()
 
-    def deactivateApp( self, appName ):
+    def deactivateApp( self, appName, check=True ):
         """
         Deactivate an app that is already activated in ONOS
+        appName is the hierarchical app name, not the feature name
+        If check is True, method will check the status of the app after the
+        command is issued
         Returns main.TRUE if the command was successfully sent
                 main.FALSE if the cli responded with an error or given
                     incorrect input
@@ -2513,7 +2530,16 @@
                 return main.TRUE
             elif status == "ACTIVE":
                 response = self.app( appName, "deactivate" )
-                return response
+                if check and response == main.TRUE:
+                    for i in range(10):  # try 10 times then give up
+                        status = self.appStatus( appName )
+                        if status == "INSTALLED":
+                            return main.TRUE
+                        else:
+                            time.sleep( 1 )
+                    return main.FALSE
+                else:  # not check or command didn't succeed
+                    return response
             elif status == "UNINSTALLED":
                 main.log.warn( self.name + ": Tried to deactivate the " +
                                 "application '" + appName + "' which is not " +
@@ -2536,9 +2562,12 @@
             main.cleanup()
             main.exit()
 
-    def uninstallApp( self, appName ):
+    def uninstallApp( self, appName, check=True ):
         """
         Uninstall an app that is already installed in ONOS
+        appName is the hierarchical app name, not the feature name
+        If check is True, method will check the status of the app after the
+        command is issued
         Returns main.TRUE if the command was successfully sent
                 main.FALSE if the cli responded with an error or given
                     incorrect input
@@ -2552,13 +2581,31 @@
             status = self.appStatus( appName )
             if status == "INSTALLED":
                 response = self.app( appName, "uninstall" )
-                return response
+                if check and response == main.TRUE:
+                    for i in range(10):  # try 10 times then give up
+                        status = self.appStatus( appName )
+                        if status == "UNINSTALLED":
+                            return main.TRUE
+                        else:
+                            time.sleep( 1 )
+                    return main.FALSE
+                else:  # not check or command didn't succeed
+                    return response
             elif status == "ACTIVE":
                 main.log.warn( self.name + ": Tried to uninstall the " +
                                 "application '" + appName + "' which is " +
                                 "currently active." )
                 response = self.app( appName, "uninstall" )
-                return response
+                if check and response == main.TRUE:
+                    for i in range(10):  # try 10 times then give up
+                        status = self.appStatus( appName )
+                        if status == "UNINSTALLED":
+                            return main.TRUE
+                        else:
+                            time.sleep( 1 )
+                    return main.FALSE
+                else:  # not check or command didn't succeed
+                    return response
             elif status == "UNINSTALLED":
                 return main.TRUE
             else:
@@ -2577,3 +2624,113 @@
             main.log.exception( self.name + ": Uncaught exception!" )
             main.cleanup()
             main.exit()
+
+    def appIDs( self, jsonFormat=True ):
+        """
+        Show the mappings between app id and app names given by the 'app-ids'
+        cli command
+        """
+        try:
+            cmdStr = "app-ids"
+            if jsonFormat:
+                cmdStr += " -j"
+                output = self.sendline( cmdStr )
+                assert "Error executing command" not in output
+                ansiEscape = re.compile( r'\r\r\n\x1b[^m]*m' )
+                cleanedOutput = ansiEscape.sub( '', output )
+                return cleanedOutput
+            else:
+                output = self.sendline( cmdStr )
+                assert "Error executing command" not in output
+                return output
+        except AssertionError:
+            main.log.error( "Error in processing onos:app-ids command: " +
+                            str( output ) )
+            return None
+        except TypeError:
+            main.log.exception( self.name + ": Object not as expected" )
+            return None
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":    " + self.handle.before )
+            main.cleanup()
+            main.exit()
+        except:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanup()
+            main.exit()
+
+    def appToIDCheck( self ):
+        """
+        This method will check that each application's ID listed in 'apps' is
+        the same as the ID listed in 'app-ids'. The check will also check that
+        there are no duplicate IDs issued. Note that an app ID should be
+        a globaly unique numerical identifier for app/app-like features. Once
+        an ID is registered, the ID is never freed up so that if an app is
+        reinstalled it will have the same ID.
+
+        Returns: main.TRUE  if the check passes and
+                 main.FALSE if the check fails or
+                 main.ERROR if there is some error in processing the test
+        """
+        try:
+            ids = json.loads( self.appIDs( jsonFormat=True ) )
+            apps = json.loads( self.apps( jsonFormat=True ) )
+            result = main.TRUE
+            for app in apps:
+                appID = app.get( 'id' )
+                if appID is None:
+                    main.log.error( "Error parsing app: " + str( app ) )
+                    result = main.FALSE
+                appName = app.get( 'name' )
+                if appName is None:
+                    main.log.error( "Error parsing app: " + str( app ) )
+                    result = main.FALSE
+                # get the entry in ids that has the same appID
+                current = filter(lambda item: item[ 'id' ] == appID, ids)
+                main.log.debug( "Comparing " + str( app ) + " to " +
+                                str( current ) )
+                if not current:  # if ids doesn't have this id
+                    result = main.FALSE
+                    main.log.error( "'app-ids' does not have the ID for " +
+                                    str( appName ) + " that apps does." )
+                elif len( current ) > 1:
+                    # there is more than one app with this ID
+                    result = main.FALSE
+                    # We will log this later in the method
+                elif not current[0][ 'name' ] == appName:
+                    currentName = current[0][ 'name' ]
+                    result = main.FALSE
+                    main.log.error( "'app-ids' has " + str( currentName ) +
+                                    " registered under id:" + str( appID ) +
+                                    " but 'apps' has " + str( appName ) )
+                else:
+                    pass  # id and name match!
+            # now make sure that app-ids has no duplicates
+            idsList = []
+            namesList = []
+            for item in ids:
+                idsList.append( item[ 'id' ] )
+                namesList.append( item[ 'name' ] )
+            if len( idsList ) != len( set( idsList ) ) or\
+               len( namesList ) != len( set( namesList ) ):
+                    main.log.error( "'app-ids' has some duplicate entries: \n"
+                                    + json.dumps( ids,
+                                                  sort_keys=True,
+                                                  indent=4,
+                                                  separators=( ',', ': ' ) ) )
+                    result = main.FALSE
+            return result
+        except ( ValueError, TypeError ):
+            main.log.exception( self.name + ": Object not as expected" )
+            return main.ERROR
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":    " + self.handle.before )
+            main.cleanup()
+            main.exit()
+        except:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanup()
+            main.exit()
+