| Index: chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java
|
| diff --git a/chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java b/chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..442a1f0018a5c133bc1da6cb4bc6f4003ca0b225
|
| --- /dev/null
|
| +++ b/chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java
|
| @@ -0,0 +1,333 @@
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package org.chromium.webapk.shell_apk;
|
| +
|
| +import android.content.ComponentName;
|
| +import android.content.Context;
|
| +import android.content.Intent;
|
| +import android.content.ServiceConnection;
|
| +import android.content.pm.PackageManager.NameNotFoundException;
|
| +import android.os.FileObserver;
|
| +import android.os.IBinder;
|
| +import android.os.RemoteException;
|
| +import android.test.InstrumentationTestCase;
|
| +import android.test.suitebuilder.annotation.MediumTest;
|
| +
|
| +import dalvik.system.DexFile;
|
| +
|
| +import org.chromium.base.FileUtils;
|
| +import org.chromium.content.browser.test.util.CallbackHelper;
|
| +import org.chromium.webapk.shell_apk.test.dex_optimizer.IDexOptimizerService;
|
| +
|
| +import java.io.File;
|
| +import java.util.ArrayList;
|
| +import java.util.Arrays;
|
| +
|
| +public class DexLoaderTest extends InstrumentationTestCase {
|
| + /**
|
| + * Package of APK to load dex file from and package which provides DexOptimizerService.
|
| + */
|
| + private static final String DEX_OPTIMIZER_SERVICE_PACKAGE =
|
| + "org.chromium.webapk.shell_apk.test.dex_optimizer";
|
| +
|
| + /**
|
| + * Class which implements DexOptimizerService.
|
| + */
|
| + private static final String DEX_OPTIMIZER_SERVICE_CLASS_NAME =
|
| + "org.chromium.webapk.shell_apk.test.dex_optimizer.DexOptimizerServiceImpl";
|
| +
|
| + /**
|
| + * Name of the dex file in DexOptimizer.apk.
|
| + */
|
| + private static final String DEX_ASSET_NAME = "canary.dex";
|
| +
|
| + /**
|
| + * Class to load to check whether dex is valid.
|
| + */
|
| + private static final String CANARY_CLASS_NAME =
|
| + "org.chromium.webapk.shell_apk.test.canary.Canary";
|
| +
|
| + private Context mContext;
|
| + private Context mRemoteContext;
|
| + private File mLocalDexDir;
|
| + private IDexOptimizerService mDexOptimizerService;
|
| + private ServiceConnection mServiceConnection;
|
| +
|
| + /**
|
| + * Monitors read files and modified files in the directory passed to the constructor.
|
| + */
|
| + private static class FileMonitor extends FileObserver {
|
| + public ArrayList<String> mReadPaths = new ArrayList<String>();
|
| + public ArrayList<String> mModifiedPaths = new ArrayList<String>();
|
| +
|
| + public FileMonitor(File directory) {
|
| + super(directory.getPath());
|
| + }
|
| +
|
| + @Override
|
| + public void onEvent(int event, String path) {
|
| + switch (event) {
|
| + case FileObserver.ACCESS:
|
| + mReadPaths.add(path);
|
| + break;
|
| + case FileObserver.CREATE:
|
| + case FileObserver.DELETE:
|
| + case FileObserver.DELETE_SELF:
|
| + case FileObserver.MODIFY:
|
| + mModifiedPaths.add(path);
|
| + break;
|
| + default:
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void setUp() {
|
| + mContext = getInstrumentation().getTargetContext();
|
| + mRemoteContext = getRemoteContext(mContext);
|
| +
|
| + mLocalDexDir = mContext.getDir("dex", Context.MODE_PRIVATE);
|
| + if (mLocalDexDir.exists()) {
|
| + FileUtils.recursivelyDeleteFile(mLocalDexDir);
|
| + if (mLocalDexDir.exists()) {
|
| + fail("Could not delete local dex directory.");
|
| + }
|
| + }
|
| +
|
| + connectToDexOptimizerService();
|
| +
|
| + try {
|
| + if (!mDexOptimizerService.deleteDexDirectory()) {
|
| + fail("Could not delete remote dex directory.");
|
| + }
|
| + } catch (RemoteException e) {
|
| + e.printStackTrace();
|
| + fail("Remote crashed during setup.");
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void tearDown() throws Exception {
|
| + mContext.unbindService(mServiceConnection);
|
| + super.tearDown();
|
| + }
|
| +
|
| + /**
|
| + * Test that {@DexLoader#load()} can create a ClassLoader from a dex and optimized dex in
|
| + * another app's data directory.
|
| + */
|
| + @MediumTest
|
| + public void testLoadFromRemoteDataDir() {
|
| + // Extract the dex file into another app's data directory and optimize the dex.
|
| + String remoteDexFilePath = null;
|
| + try {
|
| + remoteDexFilePath = mDexOptimizerService.extractAndOptimizeDex();
|
| + } catch (RemoteException e) {
|
| + e.printStackTrace();
|
| + fail("Remote crashed.");
|
| + }
|
| +
|
| + if (remoteDexFilePath == null) {
|
| + fail("Could not extract and optimize dex.");
|
| + }
|
| +
|
| + // Check that the Android OS knows about the optimized dex file for
|
| + // {@link remoteDexFilePath}.
|
| + File remoteDexFile = new File(remoteDexFilePath);
|
| + assertFalse(isDexOptNeeded(remoteDexFile));
|
| +
|
| + ClassLoader loader = DexLoader.load(
|
| + mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, remoteDexFile, mLocalDexDir);
|
| + assertNotNull(loader);
|
| + assertTrue(canLoadCanaryClass(loader));
|
| +
|
| + // Check that {@link DexLoader#load()} did not use the fallback path.
|
| + assertFalse(mLocalDexDir.exists());
|
| + }
|
| +
|
| + /**
|
| + * That that {@link DexLoader#load()} falls back to extracting the dex from the APK to the
|
| + * local data directory and creating the ClassLoader from the extracted dex if creating the
|
| + * ClassLoader from the cached data in the remote Context's data directory fails.
|
| + */
|
| + @MediumTest
|
| + public void testLoadFromLocalDataDir() {
|
| + ClassLoader loader = DexLoader.load(
|
| + mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
|
| + assertNotNull(loader);
|
| + assertTrue(canLoadCanaryClass(loader));
|
| +
|
| + // Check that the dex file was extracted to the local data directory and that a directory
|
| + // was created for the optimized dex.
|
| + assertTrue(mLocalDexDir.exists());
|
| + File[] localDexDirFiles = mLocalDexDir.listFiles();
|
| + assertNotNull(localDexDirFiles);
|
| + Arrays.sort(localDexDirFiles);
|
| + assertEquals(2, localDexDirFiles.length);
|
| + assertEquals(DEX_ASSET_NAME, localDexDirFiles[0].getName());
|
| + assertFalse(localDexDirFiles[0].isDirectory());
|
| + assertEquals("optimized", localDexDirFiles[1].getName());
|
| + assertTrue(localDexDirFiles[1].isDirectory());
|
| + }
|
| +
|
| + /**
|
| + * Test that {@link DexLoader#load()} does not extract the dex file from the APK if the dex file
|
| + * was extracted in a previous call to {@link DexLoader#load()}
|
| + */
|
| + @MediumTest
|
| + public void testPreviouslyLoadedFromLocalDataDir() {
|
| + assertTrue(mLocalDexDir.mkdir());
|
| +
|
| + {
|
| + // Load dex the first time. This should extract the dex file from the APK's assets and
|
| + // generate the optimized dex file.
|
| + FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
|
| + localDexDirMonitor.startWatching();
|
| + ClassLoader loader = DexLoader.load(
|
| + mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
|
| + localDexDirMonitor.stopWatching();
|
| +
|
| + assertNotNull(loader);
|
| + assertTrue(canLoadCanaryClass(loader));
|
| +
|
| + assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
|
| + assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME));
|
| + }
|
| + {
|
| + // Load dex a second time. We should use the already extracted dex file.
|
| + FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
|
| + localDexDirMonitor.startWatching();
|
| + ClassLoader loader = DexLoader.load(
|
| + mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
|
| + localDexDirMonitor.stopWatching();
|
| +
|
| + // The returned ClassLoader should be valid.
|
| + assertNotNull(loader);
|
| + assertTrue(canLoadCanaryClass(loader));
|
| +
|
| + // We should not have modified any files and have used the already extracted dex file.
|
| + assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
|
| + assertTrue(localDexDirMonitor.mModifiedPaths.isEmpty());
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Test that {@link DexLoader#load()} re-extracts the dex file from the APK after a call to
|
| + * {@link DexLoader#deleteCachedDexes()}.
|
| + */
|
| + @MediumTest
|
| + public void testLoadAfterDeleteCachedDexes() {
|
| + assertTrue(mLocalDexDir.mkdir());
|
| +
|
| + {
|
| + // Load dex the first time. This should extract the dex file from the APK's assets and
|
| + // generate the optimized dex file.
|
| + FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
|
| + localDexDirMonitor.startWatching();
|
| + ClassLoader loader = DexLoader.load(
|
| + mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
|
| + localDexDirMonitor.stopWatching();
|
| +
|
| + assertNotNull(loader);
|
| + assertTrue(canLoadCanaryClass(loader));
|
| +
|
| + assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
|
| + assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME));
|
| + }
|
| +
|
| + DexLoader.deleteCachedDexes(mLocalDexDir);
|
| +
|
| + {
|
| + // Load dex a second time.
|
| + FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
|
| + localDexDirMonitor.startWatching();
|
| + ClassLoader loader = DexLoader.load(
|
| + mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
|
| + localDexDirMonitor.stopWatching();
|
| +
|
| + // The returned ClassLoader should be valid.
|
| + assertNotNull(loader);
|
| + assertTrue(canLoadCanaryClass(loader));
|
| +
|
| + // We should have re-extracted the dex from the APK's assets.
|
| + assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
|
| + assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME));
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Connects to the DexOptimizerService.
|
| + */
|
| + private void connectToDexOptimizerService() {
|
| + Intent intent = new Intent();
|
| + intent.setComponent(
|
| + new ComponentName(DEX_OPTIMIZER_SERVICE_PACKAGE, DEX_OPTIMIZER_SERVICE_CLASS_NAME));
|
| + final CallbackHelper connectedCallback = new CallbackHelper();
|
| +
|
| + mServiceConnection = new ServiceConnection() {
|
| + @Override
|
| + public void onServiceConnected(ComponentName name, IBinder service) {
|
| + mDexOptimizerService = IDexOptimizerService.Stub.asInterface(service);
|
| + connectedCallback.notifyCalled();
|
| + }
|
| +
|
| + @Override
|
| + public void onServiceDisconnected(ComponentName name) {}
|
| + };
|
| +
|
| + try {
|
| + mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
|
| + } catch (SecurityException e) {
|
| + e.printStackTrace();
|
| + fail();
|
| + }
|
| +
|
| + try {
|
| + connectedCallback.waitForCallback(0);
|
| + } catch (Exception e) {
|
| + e.printStackTrace();
|
| + fail("Could not connect to remote.");
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Returns the Context of the APK which provides DexOptimizerService.
|
| + * @param context The test application's Context.
|
| + * @return Context of the APK whcih provide DexOptimizerService.
|
| + */
|
| + private Context getRemoteContext(Context context) {
|
| + try {
|
| + return context.getApplicationContext().createPackageContext(
|
| + DEX_OPTIMIZER_SERVICE_PACKAGE,
|
| + Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
|
| + } catch (NameNotFoundException e) {
|
| + e.printStackTrace();
|
| + fail("Could not get remote context");
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + /** Returns whether the Android OS thinks that a dex file needs to be re-optimized */
|
| + private boolean isDexOptNeeded(File dexFile) {
|
| + try {
|
| + return DexFile.isDexOptNeeded(dexFile.getPath());
|
| + } catch (Exception e) {
|
| + e.printStackTrace();
|
| + fail();
|
| + return false;
|
| + }
|
| + }
|
| +
|
| + /** Returns whether the ClassLoader can load {@link CANARY_CLASS_NAME} */
|
| + private boolean canLoadCanaryClass(ClassLoader loader) {
|
| + try {
|
| + loader.loadClass(CANARY_CLASS_NAME);
|
| + return true;
|
| + } catch (Exception e) {
|
| + return false;
|
| + }
|
| + }
|
| +}
|
|
|