// Copyright 2016 The Chromium Authors
// 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.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.FileObserver;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.FileUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import java.io.File;
import java.util.ArrayList;
/** Tests for {@link DexLoader}. */
@RunWith(BaseJUnit4ClassRunner.class)
public class DexLoaderTest {
/** Package of APK to load dex file from. */
private static final String PACKAGE_WITH_DEX_TO_EXTRACT =
"org.chromium.webapk.shell_apk.test.dex_optimizer";
/** Name of dex files in DexOptimizer.apk. */
private static final String DEX_ASSET_NAME = "canary.dex";
private static final String DEX_ASSET_NAME2 = "canary2.dex";
/** Classes to load to check whether dex is valid. */
private static final String CANARY_CLASS_NAME =
"org.chromium.webapk.shell_apk.test.canary.Canary";
private static final String CANARY_CLASS_NAME2 =
"org.chromium.webapk.shell_apk.test.canary.Canary2";
private Context mContext;
private Context mRemoteContext;
private DexLoader mDexLoader;
private File mLocalDexDir;
/** 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;
}
}
}
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
mRemoteContext = getRemoteContext(mContext);
mDexLoader = new DexLoader();
mLocalDexDir = mContext.getDir("dex", Context.MODE_PRIVATE);
if (mLocalDexDir.exists()) {
FileUtils.recursivelyDeleteFile(mLocalDexDir, FileUtils.DELETE_ALL);
if (mLocalDexDir.exists()) {
Assert.fail("Could not delete local dex directory.");
}
}
}
@Test
@MediumTest
public void testBasic() {
ClassLoader loader =
mDexLoader.load(mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, mLocalDexDir);
Assert.assertNotNull(loader);
Assert.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.
Assert.assertTrue(mLocalDexDir.exists());
File[] localDexDirFiles = mLocalDexDir.listFiles();
Assert.assertNotNull(localDexDirFiles);
boolean foundDexFile = false;
boolean foundOptimizedDir = false;
for (File f : localDexDirFiles) {
if (f.isDirectory()) {
if (f.getName().equals("optimized")) {
foundOptimizedDir = true;
}
} else if (f.getName().equals(DEX_ASSET_NAME)) {
foundDexFile = true;
}
}
Assert.assertTrue(foundDexFile);
Assert.assertTrue(foundOptimizedDir);
}
/**
* 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()}
*/
@Test
@MediumTest
public void testPreviouslyLoadedFromLocalDataDir() {
Assert.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 =
mDexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, mLocalDexDir);
localDexDirMonitor.stopWatching();
Assert.assertNotNull(loader);
Assert.assertTrue(canLoadCanaryClass(loader));
Assert.assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
Assert.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 =
mDexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, mLocalDexDir);
localDexDirMonitor.stopWatching();
// The returned ClassLoader should be valid.
Assert.assertNotNull(loader);
Assert.assertTrue(canLoadCanaryClass(loader));
// The modified files (if any) should be .flock lock files.
for (String modifiedPath : localDexDirMonitor.mModifiedPaths) {
Assert.assertTrue(modifiedPath.endsWith(".flock"));
}
}
}
/**
* Test loading a dex file from a directory which was previously used for loading a different
* dex file.
*/
@Test
@MediumTest
public void testLoadDifferentDexInLocalDataDir() {
Assert.assertTrue(mLocalDexDir.mkdir());
// Load canary.dex
ClassLoader loader1 =
mDexLoader.load(mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, mLocalDexDir);
Assert.assertNotNull(loader1);
Assert.assertTrue(canLoadCanaryClass(loader1));
File canaryDexFile1 = new File(mLocalDexDir, DEX_ASSET_NAME);
Assert.assertTrue(canaryDexFile1.exists());
mDexLoader.deleteCachedDexes(mLocalDexDir);
ClassLoader loader2 =
mDexLoader.load(mRemoteContext, DEX_ASSET_NAME2, CANARY_CLASS_NAME2, mLocalDexDir);
Assert.assertNotNull(loader2);
Assert.assertTrue(canLoadClass(loader2, CANARY_CLASS_NAME2));
// canary2.dex should have been extracted and the previously extracted canary.dex file
// should have been deleted.
Assert.assertTrue(new File(mLocalDexDir, DEX_ASSET_NAME2).exists());
Assert.assertFalse(canaryDexFile1.exists());
}
/**
* Returns the Context of the APK which contains dex with canary class implementation.
*
* @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(
PACKAGE_WITH_DEX_TO_EXTRACT,
Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
} catch (NameNotFoundException e) {
e.printStackTrace();
Assert.fail("Could not get remote context");
return null;
}
}
/** Returns whether the ClassLoader can load {@link CANARY_CLASS_NAME} */
private boolean canLoadCanaryClass(ClassLoader loader) {
return canLoadClass(loader, CANARY_CLASS_NAME);
}
/** Returns whether the ClassLoader can load a class */
private boolean canLoadClass(ClassLoader loader, String className) {
try {
loader.loadClass(className);
return true;
} catch (Exception e) {
return false;
}
}
}