diff options
Diffstat (limited to 'libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java')
-rw-r--r-- | libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java b/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java new file mode 100644 index 0000000000..c9290377ee --- /dev/null +++ b/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test.hostside; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.invoker.IInvocationContext; +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.IAbi; +import com.android.tradefed.testtype.junit4.AfterClassWithInfo; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.testtype.junit4.BeforeClassWithInfo; +import com.android.tradefed.util.CommandResult; +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test libnativeloader behavior for apps and libs in various partitions by overlaying them over + * the system partitions. Requires root. + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class LibnativeloaderTest extends BaseHostJUnit4Test { + private static final String TAG = "LibnativeloaderTest"; + private static final String CLEANUP_PATHS_KEY = TAG + ":CLEANUP_PATHS"; + private static final String LOG_FILE_NAME = "TestActivity.log"; + + @BeforeClassWithInfo + public static void beforeClassWithDevice(TestInformation testInfo) throws Exception { + DeviceContext ctx = new DeviceContext( + testInfo.getContext(), testInfo.getDevice(), testInfo.getBuildInfo()); + + // A soft reboot is slow, so do setup for all tests and reboot once. + + ctx.mDevice.remountSystemWritable(); + + File libContainerApk = ctx.mBuildHelper.getTestFile("library_container_app.apk"); + try (ZipFile libApk = new ZipFile(libContainerApk)) { + ctx.pushExtendedPublicSystemOemLibs(libApk); + ctx.pushExtendedPublicProductLibs(libApk); + ctx.pushPrivateLibs(libApk); + } + ctx.pushSystemSharedLib("/system/framework", "android.test.systemsharedlib", + "libnativeloader_system_shared_lib.jar"); + ctx.pushSystemSharedLib("/system_ext/framework", "android.test.systemextsharedlib", + "libnativeloader_system_ext_shared_lib.jar"); + + // "Install" apps in various partitions through plain adb push followed by a soft reboot. We + // need them in these locations to test library loading restrictions, so for all except + // loadlibrarytest_data_app we cannot use ITestDevice.installPackage for it since it only + // installs in /data. + + // For testSystemPrivApp + ctx.pushApk("loadlibrarytest_system_priv_app", "/system/priv-app"); + + // For testSystemApp + ctx.pushApk("loadlibrarytest_system_app", "/system/app"); + + // For testSystemExtApp + ctx.pushApk("loadlibrarytest_system_ext_app", "/system_ext/app"); + + // For testProductApp + ctx.pushApk("loadlibrarytest_product_app", "/product/app"); + + // For testVendorApp + ctx.pushApk("loadlibrarytest_vendor_app", "/vendor/app"); + + ctx.softReboot(); + + // For testDataApp. Install this the normal way after the system server restart. + ctx.installPackage("loadlibrarytest_data_app"); + + testInfo.properties().put(CLEANUP_PATHS_KEY, ctx.mCleanup.getPathList()); + } + + @AfterClassWithInfo + public static void afterClassWithDevice(TestInformation testInfo) throws Exception { + ITestDevice device = testInfo.getDevice(); + + // Uninstall loadlibrarytest_data_app. + device.uninstallPackage("android.test.app.data"); + + String cleanupPathList = testInfo.properties().get(CLEANUP_PATHS_KEY); + CleanupPaths cleanup = new CleanupPaths(device, cleanupPathList); + cleanup.cleanup(); + } + + @Test + public void testSystemPrivApp() throws Exception { + // There's currently no difference in the tests between /system/priv-app and /system/app, so + // let's reuse the same one. + runDeviceTests("android.test.app.system_priv", "android.test.app.SystemAppTest"); + } + + @Test + public void testSystemApp() throws Exception { + runDeviceTests("android.test.app.system", "android.test.app.SystemAppTest"); + } + + @Test + public void testSystemExtApp() throws Exception { + // /system_ext should behave the same as /system, so run the same test class there. + runDeviceTests("android.test.app.system_ext", "android.test.app.SystemAppTest"); + } + + @Test + public void testProductApp() throws Exception { + runDeviceTests("android.test.app.product", "android.test.app.ProductAppTest"); + } + + @Test + public void testVendorApp() throws Exception { + runDeviceTests("android.test.app.vendor", "android.test.app.VendorAppTest"); + } + + @Test + public void testDataApp() throws Exception { + runDeviceTests("android.test.app.data", "android.test.app.DataAppTest"); + } + + // Utility class that keeps track of a set of paths the need to be deleted after testing. + private static class CleanupPaths { + private ITestDevice mDevice; + private List<String> mCleanupPaths; + + CleanupPaths(ITestDevice device) { + mDevice = device; + mCleanupPaths = new ArrayList<String>(); + } + + CleanupPaths(ITestDevice device, String pathList) { + mDevice = device; + mCleanupPaths = Arrays.asList(pathList.split(":")); + } + + String getPathList() { return String.join(":", mCleanupPaths); } + + // Adds the given path, or its topmost nonexisting parent directory, to the list of paths to + // clean up. + void addPath(String devicePath) throws DeviceNotAvailableException { + File path = new File(devicePath); + while (true) { + File parentPath = path.getParentFile(); + if (parentPath == null || mDevice.doesFileExist(parentPath.toString())) { + break; + } + path = parentPath; + } + String nonExistingPath = path.toString(); + if (!mCleanupPaths.contains(nonExistingPath)) { + mCleanupPaths.add(nonExistingPath); + } + } + + void cleanup() throws DeviceNotAvailableException { + // Clean up in reverse order in case several pushed files were in the same nonexisting + // directory. + for (int i = mCleanupPaths.size() - 1; i >= 0; --i) { + mDevice.deleteFile(mCleanupPaths.get(i)); + } + } + } + + // Class for code that needs an ITestDevice. It is instantiated both in tests and in + // (Before|After)ClassWithInfo. + private static class DeviceContext implements AutoCloseable { + IInvocationContext mContext; + ITestDevice mDevice; + CompatibilityBuildHelper mBuildHelper; + CleanupPaths mCleanup; + private String mTestArch; + + DeviceContext(IInvocationContext context, ITestDevice device, IBuildInfo buildInfo) { + mContext = context; + mDevice = device; + mBuildHelper = new CompatibilityBuildHelper(buildInfo); + mCleanup = new CleanupPaths(mDevice); + } + + public void close() throws DeviceNotAvailableException { mCleanup.cleanup(); } + + void pushExtendedPublicSystemOemLibs(ZipFile libApk) throws Exception { + pushNativeTestLib(libApk, "/system/${LIB}/libfoo.oem1.so"); + pushNativeTestLib(libApk, "/system/${LIB}/libbar.oem1.so"); + pushString("libfoo.oem1.so\n" + + "libbar.oem1.so\n", + "/system/etc/public.libraries-oem1.txt"); + + pushNativeTestLib(libApk, "/system/${LIB}/libfoo.oem2.so"); + pushNativeTestLib(libApk, "/system/${LIB}/libbar.oem2.so"); + pushString("libfoo.oem2.so\n" + + "libbar.oem2.so\n", + "/system/etc/public.libraries-oem2.txt"); + } + + void pushExtendedPublicProductLibs(ZipFile libApk) throws Exception { + pushNativeTestLib(libApk, "/product/${LIB}/libfoo.product1.so"); + pushNativeTestLib(libApk, "/product/${LIB}/libbar.product1.so"); + pushString("libfoo.product1.so\n" + + "libbar.product1.so\n", + "/product/etc/public.libraries-product1.txt"); + } + + void pushPrivateLibs(ZipFile libApk) throws Exception { + // Push the libraries once for each test. Since we cannot unload them, we need a fresh + // never-before-loaded library in each loadLibrary call. + for (int i = 1; i <= 3; ++i) { + pushNativeTestLib(libApk, "/system/${LIB}/libsystem_private" + i + ".so"); + pushNativeTestLib(libApk, "/system_ext/${LIB}/libsystemext_private" + i + ".so"); + pushNativeTestLib(libApk, "/product/${LIB}/libproduct_private" + i + ".so"); + pushNativeTestLib(libApk, "/vendor/${LIB}/libvendor_private" + i + ".so"); + } + } + + void pushSystemSharedLib(String packageDir, String packageName, String buildJarName) + throws Exception { + String path = packageDir + "/" + packageName + ".jar"; + pushFile(buildJarName, path); + pushString("<permissions>\n" + + "<library name=\"" + packageName + "\" file=\"" + path + "\" />\n" + + "</permissions>\n", + "system/etc/permissions/" + packageName + ".xml"); + } + + void softReboot() throws DeviceNotAvailableException { + assertCommandSucceeds("setprop dev.bootcomplete 0"); + assertCommandSucceeds("stop"); + assertCommandSucceeds("start"); + mDevice.waitForDeviceAvailable(); + } + + String getTestArch() throws DeviceNotAvailableException { + if (mTestArch == null) { + IAbi abi = mContext.getConfigurationDescriptor().getAbi(); + mTestArch = abi != null ? abi.getName() + : assertCommandSucceeds("getprop ro.bionic.arch"); + } + return mTestArch; + } + + // Pushes the given file contents to the device at the given destination path. destPath is + // assumed to have no risk of overlapping with existing files, and is deleted in tearDown(), + // along with any directory levels that had to be created. + void pushString(String fileContents, String destPath) throws DeviceNotAvailableException { + mCleanup.addPath(destPath); + assertThat(mDevice.pushString(fileContents, destPath)).isTrue(); + } + + // Like pushString, but pushes a data file included in the host test. + void pushFile(String fileName, String destPath) throws Exception { + mCleanup.addPath(destPath); + assertThat(mDevice.pushFile(mBuildHelper.getTestFile(fileName), destPath)).isTrue(); + } + + void pushApk(String apkBaseName, String destPath) throws Exception { + pushFile(apkBaseName + ".apk", + destPath + "/" + apkBaseName + "/" + apkBaseName + ".apk"); + } + + // Like pushString, but extracts libnativeloader_testlib.so from the library_container_app + // APK and pushes it to destPath. "${LIB}" is replaced with "lib" or "lib64" as appropriate. + void pushNativeTestLib(ZipFile libApk, String destPath) throws Exception { + String libApkPath = "lib/" + getTestArch() + "/libnativeloader_testlib.so"; + ZipEntry entry = libApk.getEntry(libApkPath); + assertWithMessage("Failed to find " + libApkPath + " in library_container_app.apk") + .that(entry) + .isNotNull(); + + File libraryTempFile; + try (InputStream inStream = libApk.getInputStream(entry)) { + libraryTempFile = writeStreamToTempFile("libnativeloader_testlib.so", inStream); + } + + String libDir = getTestArch().contains("64") ? "lib64" : "lib"; + destPath = destPath.replace("${LIB}", libDir); + + mCleanup.addPath(destPath); + assertThat(mDevice.pushFile(libraryTempFile, destPath)).isTrue(); + } + + void installPackage(String apkBaseName) throws Exception { + assertThat(mDevice.installPackage(mBuildHelper.getTestFile(apkBaseName + ".apk"), + false /* reinstall */)) + .isNull(); + } + + String assertCommandSucceeds(String command) throws DeviceNotAvailableException { + CommandResult result = mDevice.executeShellV2Command(command); + assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); + // Remove trailing \n's. + return result.getStdout().trim(); + } + } + + static private File writeStreamToTempFile(String tempFileBaseName, InputStream inStream) + throws Exception { + File hostTempFile = File.createTempFile(tempFileBaseName, null); + try (FileOutputStream outStream = new FileOutputStream(hostTempFile)) { + ByteStreams.copy(inStream, outStream); + } + return hostTempFile; + } +} |