1# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2# SPDX-License-Identifier: Apache-2.0.
3#
4
5import re
6import os
7import argparse
8import subprocess
9import shutil
10import time
11import datetime
12import sys
13
14TestName = "AndroidSDKTesting"
15TestLowerName = TestName.lower()
16
17def ArgumentException( Exception ):
18    def __init__( self, argumentName, argumentValue ):
19        self.m_argumentName = argumentName
20        self.m_argumentValue = argumentValue
21
22
23def ParseArguments():
24    parser = argparse.ArgumentParser(description="AWSNativeSDK Android Test Script")
25    parser.add_argument("--clean", action="store_true")
26    parser.add_argument("--emu", action="store_true")
27    parser.add_argument("--abi", action="store")
28    parser.add_argument("--avd", action="store")
29    parser.add_argument("--nobuild", action="store_true")
30    parser.add_argument("--noinstall", action="store_true")
31    parser.add_argument("--runtest", action="store")
32    parser.add_argument("--credentials", action="store")
33    parser.add_argument("--build", action="store")
34    parser.add_argument("--so", action="store_true")
35    parser.add_argument("--stl", action="store")
36
37    args = vars( parser.parse_args() )
38
39    argMap = {}
40    argMap[ "clean" ] = args[ "clean" ]
41    argMap[ "abi" ] = args[ "abi" ] or "armeabi-v7a"
42    argMap[ "avd" ] = args[ "avd" ]
43    argMap[ "useExistingEmulator" ] = args[ "emu" ]
44    argMap[ "noBuild" ] = args[ "nobuild" ]
45    argMap[ "noInstall" ] = args[ "noinstall" ]
46    argMap[ "credentialsFile" ] = args[ "credentials" ] or "~/.aws/credentials"
47    argMap[ "buildType" ] = args[ "build" ] or "Release"
48    argMap[ "runTest" ] = args[ "runtest" ]
49    argMap[ "so" ] = args[ "so" ]
50    argMap[ "stl" ] = args[ "stl" ] or "libc++_shared"
51
52    return argMap
53
54
55def IsValidABI(abi):
56    return abi == "armeabi-v7a"
57
58
59def ShouldBuildClean(abi, buildDir):
60    if not os.path.exists( buildDir ):
61        return True
62
63    abiPattern = re.compile("ANDROID_ABI:STRING=\s*(?P<abi>\S+)")
64    for _, line in enumerate(open(buildDir + "/CMakeCache.txt")):
65        result = abiPattern.search(line)
66        if result != None:
67            return result.group("abi") != abi
68
69    return False
70
71
72def BuildAvdAbiSet():
73    namePattern = re.compile("Name:\s*(?P<name>\S+)")
74    abiPattern = re.compile("ABI: default/(?P<abi>\S+)")
75    avdList = subprocess.check_output(["android", "list", "avds"])
76    avdABIs = {}
77    currentName = None
78
79    for _, line in enumerate(avdList.splitlines()):
80        if not currentName:
81            nameResult = namePattern.search(line)
82            if nameResult != None:
83                currentName = nameResult.group("name")
84        else:
85            abiResult = abiPattern.search(line)
86            if abiResult != None:
87                avdABIs[currentName] = abiResult.group("abi")
88                currentName = None
89
90    return avdABIs
91
92
93def DoesAVDSupportABI(avdAbi, abi):
94    if avdAbi == "armeabi-v7a":
95        return abi == "armeabi-v7a" or abi == "armeabi"
96    else:
97        return abi == avdAbi
98
99
100def FindAVDForABI(abi, avdABIs):
101    for avdName in avdABIs:
102        if DoesAVDSupportABI(avdABIs[avdName], abi):
103            return avdName
104
105    return None
106
107
108def IsValidAVD(avd, abi, avdABIs):
109    return DoesAVDSupportABI(avdABIs[avd], abi)
110
111
112def GetTestList(buildSharedObjects):
113    if buildSharedObjects:
114        return [ 'core', 's3', 'dynamodb', 'cloudfront', 'cognitoidentity', 'identity', 'lambda', 'logging', 'redshift', 'sqs', 'transfer' ]
115    else:
116        return [ 'unified' ]
117
118
119def ValidateArguments(buildDir, avd, abi, clean, runTest, buildSharedObjects):
120
121    validTests = GetTestList( buildSharedObjects )
122    if runTest not in validTests:
123        print( 'Invalid value for runtest option: ' + runTest )
124        print( 'Valid values are: ' )
125        print( '  ' + ", ".join( validTests ) )
126        raise ArgumentException('runtest', runTest)
127
128    if not IsValidABI(abi):
129        print('Invalid argument value for abi: ', abi)
130        print('  Valid values are "armeabi-v7a"')
131        raise ArgumentException('abi', abi)
132
133    if not clean and ShouldBuildClean(abi, buildDir):
134        clean = True
135
136    avdABIs = BuildAvdAbiSet()
137
138    if not avd:
139        print('No virtual device specified (--avd), trying to find one in the existing avd set...')
140        avd = FindAVDForABI(abi, avdABIs)
141
142    if not IsValidAVD(avd, abi, avdABIs):
143        print('Invalid virtual device: ', avd)
144        print('  Use --avd to set the virtual device')
145        print('  Use "android lists avds" to see all usable virtual devices')
146        raise ArgumentException('avd', avd)
147
148    return (avd, abi, clean)
149
150
151def SetupJniDirectory(abi, clean):
152    path = os.path.join( TestName, "app", "src", "main", "jniLibs", abi )
153
154    if clean and os.path.exists(path):
155        shutil.rmtree(path)
156
157    if os.path.exists( path ) == False:
158        os.makedirs( path )
159
160    return path
161
162
163def CopyNativeLibraries(buildSharedObjects, jniDir, buildDir, abi, stl):
164    baseToolchainDir = os.path.join(buildDir, 'toolchains', 'android')
165    toolchainDirList = os.listdir(baseToolchainDir) # should only be one entry
166    toolchainDir = os.path.join(baseToolchainDir, toolchainDirList[0])
167
168    platformLibDir = os.path.join(toolchainDir, "sysroot", "usr", "lib")
169    shutil.copy(os.path.join(platformLibDir, "liblog.so"), jniDir)
170
171    stdLibDir = os.path.join(toolchainDir, 'arm-linux-androideabi', 'lib')
172    if stl == 'libc++_shared':
173        shutil.copy(os.path.join(stdLibDir, "libc++_shared.so"), jniDir)
174    elif stl == 'gnustl_shared':
175        shutil.copy(os.path.join(stdLibDir, "armv7-a", "libgnustl_shared.so"), jniDir)  # TODO: remove armv7-a hardcoded path
176
177    if buildSharedObjects:
178
179        soPattern = re.compile(".*\.so$")
180
181        for rootDir, dirNames, fileNames in os.walk( buildDir ):
182            for fileName in fileNames:
183                if soPattern.search(fileName):
184                    libFileName = os.path.join(rootDir, fileName)
185                    shutil.copy(libFileName, jniDir)
186
187    else:
188        unifiedTestsLibrary = os.path.join(buildDir, "android-unified-tests", "libandroid-unified-tests.so")
189        shutil.copy(unifiedTestsLibrary, jniDir)
190
191def RemoveTree(dir):
192    if os.path.exists( dir ):
193        shutil.rmtree( dir )
194
195
196def BuildNative(abi, clean, buildDir, jniDir, installDir, buildType, buildSharedObjects, stl):
197    if clean:
198        RemoveTree(installDir)
199        RemoveTree(buildDir)
200        RemoveTree(jniDir)
201        for externalProjectDir in [ "openssl", "zlib", "curl" ]:
202            RemoveTree(externalProjectDir)
203
204        os.makedirs( jniDir )
205        os.makedirs( buildDir )
206        os.chdir( buildDir )
207
208        if not buildSharedObjects:
209            link_type_line = "-DBUILD_SHARED_LIBS=OFF"
210        else:
211            link_type_line = "-DBUILD_SHARED_LIBS=ON"
212
213        subprocess.check_call( [ "cmake",
214                                 link_type_line,
215                                 "-DCUSTOM_MEMORY_MANAGEMENT=ON",
216                                 "-DTARGET_ARCH=ANDROID",
217                                 "-DANDROID_ABI=" + abi,
218                                 "-DANDROID_STL=" + stl,
219                                 "-DCMAKE_BUILD_TYPE=" + buildType,
220                                 "-DENABLE_UNITY_BUILD=ON",
221                                 '-DTEST_CERT_PATH="/data/data/aws.' + TestLowerName + '/certs"',
222                                 '-DBUILD_ONLY=dynamodb;sqs;s3;lambda;kinesis;cognito-identity;transfer;iam;identity-management;access-management;s3-encryption',
223                                 ".."] )
224    else:
225        os.chdir( buildDir )
226
227    if buildSharedObjects:
228        subprocess.check_call( [ "make", "-j12" ] )
229    else:
230        subprocess.check_call( [ "make", "-j12", "android-unified-tests" ] )
231
232    os.chdir( ".." )
233    CopyNativeLibraries(buildSharedObjects, jniDir, buildDir, abi, stl)
234
235
236def BuildJava(clean):
237    os.chdir( TestName )
238    if clean:
239        subprocess.check_call( [ "./gradlew", "clean" ] )
240        subprocess.check_call( [ "./gradlew", "--refresh-dependencies" ] )
241
242    subprocess.check_call( [ "./gradlew", "assembleDebug" ] )
243    os.chdir( ".." )
244
245
246def IsAnEmulatorRunning():
247    emulatorPattern = re.compile("(?P<emu>emulator-\d+)")
248    emulatorList = subprocess.check_output(["adb", "devices"])
249
250    for _, line in enumerate(emulatorList.splitlines()):
251        result = emulatorPattern.search(line)
252        if result:
253            return True
254
255    return False
256
257
258def KillRunningEmulators():
259    emulatorPattern = re.compile("(?P<emu>emulator-\d+)")
260    emulatorList = subprocess.check_output(["adb", "devices"])
261
262    for _, line in enumerate(emulatorList.splitlines()):
263        result = emulatorPattern.search(line)
264        if result:
265            emulatorName = result.group( "emu" )
266            subprocess.check_call( [ "adb", "-s", emulatorName, "emu", "kill" ] )
267
268
269def WaitForEmulatorToBoot():
270    time.sleep(5)
271    subprocess.check_call( [ "adb", "-e", "wait-for-device" ] )
272
273    print( "Device online; booting..." )
274
275    bootCompleted = False
276    bootAnimPlaying = True
277    while not bootCompleted or bootAnimPlaying:
278        time.sleep(1)
279        bootCompleted = subprocess.check_output( [ "adb", "-e", "shell", "getprop sys.boot_completed" ] ).strip() == "1"
280        bootAnimPlaying = subprocess.check_output( [ "adb", "-e", "shell", "getprop init.svc.bootanim" ] ).strip() != "stopped"
281
282    print( "Device booted" )
283
284
285def InitializeEmulator(avd, useExistingEmu):
286    if not useExistingEmu:
287        KillRunningEmulators()
288
289    if not IsAnEmulatorRunning():
290        # this may not work on windows due to the shell and &
291        subprocess.Popen( "emulator -avd " + avd + " -gpu off &", shell=True ).communicate()
292
293    WaitForEmulatorToBoot()
294
295
296#TEMPORARY: once we have android CI, we will adjust the emulator's CA set as a one-time step and then remove this step
297def BuildAndInstallCertSet(pemSourceDir, buildDir):
298    # android's default cert set does not allow verification of Amazon's cert chain, so we build, install, and use our own set that works
299    certDir = os.path.join( buildDir, "certs" )
300    pemSourceFile = os.path.join( pemSourceDir, "cacert.pem" )
301
302    # assume that if the directory exists, then the cert set is valid and we just need to upload
303    if not os.path.exists( certDir ):
304        os.makedirs( certDir )
305
306        # extract all the certs in curl's master cacert.pem file out into individual .pem files
307        subprocess.check_call( "cat " + pemSourceFile + " | awk '{print > \"" + certDir + "/cert\" (1+n) \".pem\"} /-----END CERTIFICATE-----/ {n++}'", shell = True )
308
309        # use openssl to transform the certs into the hashname form that curl/openssl expects
310        subprocess.check_call( "c_rehash certs", shell = True, cwd = buildDir )
311
312        # The root (VeriSign 3) cert in Amazon's chain is missing from curl's master cacert.pem file and needs to be copied manually
313        shutil.copy(os.path.join( pemSourceDir, "certs", "415660c1.0" ), certDir)
314        shutil.copy(os.path.join( pemSourceDir, "certs", "7651b327.0" ), certDir)
315
316    subprocess.check_call( [ "adb", "shell", "rm -rf /data/data/aws." + TestLowerName + "/certs" ] )
317    subprocess.check_call( [ "adb", "shell", "mkdir /data/data/aws." + TestLowerName + "/certs" ] )
318
319    # upload all the hashed certs to the emulator
320    certPattern = re.compile(".*\.0$")
321
322    for rootDir, dirNames, fileNames in os.walk( certDir ):
323        for fileName in fileNames:
324            if certPattern.search(fileName):
325                certFileName = os.path.join(rootDir, fileName)
326                subprocess.check_call( [ "adb", "push", certFileName, "/data/data/aws." + TestLowerName + "/certs" ] )
327
328def UploadTestResources(resourcesDir):
329    for rootDir, dirNames, fileNames in os.walk( resourcesDir ):
330        for fileName in fileNames:
331            resourceFileName = os.path.join( rootDir, fileName )
332            subprocess.check_call( [ "adb", "push", resourceFileName, os.path.join( "/data/data/aws." + TestLowerName + "/resources", fileName ) ] )
333
334def UploadAwsSigV4TestSuite(resourceDir):
335    for rootDir, dirNames, fileNames in os.walk( resourceDir ):
336        for fileName in fileNames:
337            resourceFileName = os.path.join( rootDir, fileName )
338            subDir = os.path.basename( rootDir )
339            subprocess.check_call( [ "adb", "push", resourceFileName, os.path.join( "/data/data/aws." + TestLowerName + "/resources", subDir, fileName ) ] )
340
341
342def InstallTests(credentialsFile):
343    subprocess.check_call( [ "adb", "install", "-r", TestName + "/app/build/outputs/apk/app-debug.apk" ] )
344    subprocess.check_call( [ "adb", "logcat", "-c" ] ) # this doesn't seem to work
345    if credentialsFile and credentialsFile != "":
346        print( "uploading credentials" )
347        subprocess.check_call( [ "adb", "push", credentialsFile, "/data/data/aws." + TestLowerName + "/.aws/credentials" ] )
348
349
350def TestsAreRunning(timeStart):
351    shutdownCalledOutput = subprocess.check_output( "adb logcat -t " + timeStart + " *:V | grep \"Shutting down TestActivity\"; exit 0 ", shell = True )
352    return not shutdownCalledOutput
353
354
355def RunTest(testName):
356    time.sleep(5)
357    print( "Attempting to unlock..." )
358    subprocess.check_call( [ "adb", "-e", "shell", "input keyevent 82" ] )
359
360    logTime = datetime.datetime.now() + datetime.timedelta(minutes=-1) # the emulator and the computer do not appear to be in perfect sync
361    logTimeString = logTime.strftime("\"%m-%d %H:%M:%S.000\"")
362
363    time.sleep(5)
364    print( "Attempting to run tests..." )
365    subprocess.check_call( [ "adb", "shell", "am start -e test " + testName + " -n aws." + TestLowerName + "/aws." + TestLowerName + ".RunSDKTests" ] )
366
367    time.sleep(10)
368
369    while TestsAreRunning(logTimeString):
370        print( "Tests still running..." )
371        time.sleep(5)
372
373    print( "Saving logs..." )
374    subprocess.Popen( "adb logcat -t " + logTimeString + " *:V | grep -a NativeSDK > AndroidTestOutput.txt", shell=True )
375
376    print( "Cleaning up..." )
377    subprocess.check_call( [ "adb", "shell", "pm clear aws." + TestLowerName ] )
378
379
380def DidAllTestsSucceed():
381    failures = subprocess.check_output( "grep \"FAILED\" AndroidTestOutput.txt ; exit 0", shell = True )
382    return failures == ""
383
384
385def Main():
386    args = ParseArguments()
387
388    avd = args[ "avd" ]
389    abi = args[ "abi" ]
390    clean = args[ "clean" ]
391    useExistingEmu = args[ "useExistingEmulator" ]
392    skipBuild = args[ "noBuild" ]
393    credentialsFile = args[ "credentialsFile" ]
394    buildType = args[ "buildType" ]
395    noInstall = args[ "noInstall" ]
396    buildSharedObjects = args[ "so" ]
397    runTest = args[ "runTest" ]
398    stl = args[ "stl" ]
399
400    buildDir = "_build" + buildType
401    installDir = os.path.join( "external", abi );
402
403    if runTest:
404        avd, abi, clean = ValidateArguments(buildDir, avd, abi, clean, runTest, buildSharedObjects)
405
406    jniDir = SetupJniDirectory(abi, clean)
407
408    if not skipBuild:
409        BuildNative(abi, clean, buildDir, jniDir, installDir, buildType, buildSharedObjects, stl)
410        BuildJava(clean)
411
412    if not runTest:
413        return 0
414
415    print("Starting emulator...")
416    InitializeEmulator(avd, useExistingEmu)
417
418    if not noInstall:
419        print("Installing tests...")
420        InstallTests(credentialsFile)
421
422        print("Installing certs...")
423        BuildAndInstallCertSet("android-build", buildDir)
424
425        print("Uploading test resources")
426        UploadTestResources("aws-cpp-sdk-lambda-integration-tests/resources")
427
428        print("Uploading SigV4 test files")
429        UploadAwsSigV4TestSuite(os.path.join("aws-cpp-sdk-core-tests", "resources", "aws4_testsuite", "aws4_testsuite"))
430
431    print("Running tests...")
432    RunTest( runTest )
433
434    if not useExistingEmu:
435        KillRunningEmulators()
436
437    if DidAllTestsSucceed():
438        print( "All tests passed!" )
439        return 0
440    else:
441        print( "Some tests failed.  See AndroidTestOutput.txt" )
442        return 1
443
444
445
446Main()
447