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; |
+ } |
+ } |
+} |