Merge "Add tests about MultiDex corruption recovering"

This commit is contained in:
Yohann Roussel
2018-01-31 09:57:44 +00:00
committed by Gerrit Code Review
6 changed files with 464 additions and 26 deletions

View File

@@ -36,10 +36,8 @@ LOCAL_DEX_PREOPT := false
include $(BUILD_PACKAGE)
ifndef LOCAL_JACK_ENABLED
$(mainDexList): $(full_classes_proguard_jar) | $(MAINDEXCLASSES)
$(hide) mkdir -p $(dir $@)
$(MAINDEXCLASSES) $< 1>$@
$(built_dex_intermediate): $(mainDexList)
endif

View File

@@ -7,6 +7,8 @@
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="19" />
<!-- Required for com.android.framework.multidexlegacytestservices.test2 -->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
<application
android:label="MultiDexLegacyTestServices">
@@ -124,6 +126,6 @@
<action android:name="com.android.framework.multidexlegacytestservices.action.Service19" />
</intent-filter>
</service>
</application>
</application>
</manifest>

View File

@@ -60,35 +60,40 @@ public abstract class AbstractService extends Service implements Runnable {
// of the result file will be too big.
RandomAccessFile raf = new RandomAccessFile(resultFile, "rw");
raf.seek(raf.length());
Log.i(TAG, "Writing 0x42434445 at " + raf.length() + " in " + resultFile.getPath());
raf.writeInt(0x42434445);
if (raf.length() == 0) {
Log.i(TAG, "Writing 0x42434445 at " + raf.length() + " in " + resultFile.getPath());
raf.writeInt(0x42434445);
} else {
Log.w(TAG, "Service was restarted appending 0x42434445 twice at " + raf.length()
+ " in " + resultFile.getPath());
raf.writeInt(0x42434445);
raf.writeInt(0x42434445);
}
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
MultiDex.install(applicationContext);
Log.i(TAG, "Multi dex installation done.");
MultiDex.install(applicationContext);
Log.i(TAG, "Multi dex installation done.");
int value = getValue();
Log.i(TAG, "Saving the result (" + value + ") to " + resultFile.getPath());
try {
int value = getValue();
Log.i(TAG, "Saving the result (" + value + ") to " + resultFile.getPath());
// Append the check value in result file, keeping the constant values already written.
RandomAccessFile raf = new RandomAccessFile(resultFile, "rw");
raf = new RandomAccessFile(resultFile, "rw");
raf.seek(raf.length());
Log.i(TAG, "Writing result at " + raf.length() + " in " + resultFile.getPath());
raf.writeInt(value);
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
// Writing end of processing flags, the existence of the file is the criteria
RandomAccessFile raf = new RandomAccessFile(new File(applicationContext.getFilesDir(), getId() + ".complete"), "rw");
Log.i(TAG, "creating complete file " + resultFile.getPath());
raf.writeInt(0x32333435);
raf.close();
} catch (IOException e) {
e.printStackTrace();
throw new AssertionError(e);
} finally {
try {
// Writing end of processing flags, the existence of the file is the criteria
RandomAccessFile raf = new RandomAccessFile(
new File(applicationContext.getFilesDir(), getId() + ".complete"), "rw");
Log.i(TAG, "creating complete file " + resultFile.getPath());
raf.writeInt(0x32333435);
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@@ -119,9 +124,10 @@ public abstract class AbstractService extends Service implements Runnable {
intermediate = ReflectIntermediateClass.get(45, 80, 20 /* 5 seems enough on a nakasi,
using 20 to get some margin */);
} catch (Exception e) {
e.printStackTrace();
throw new AssertionError(e);
}
int value = new com.android.framework.multidexlegacytestservices.manymethods.Big001().get1() +
int value =
new com.android.framework.multidexlegacytestservices.manymethods.Big001().get1() +
new com.android.framework.multidexlegacytestservices.manymethods.Big002().get2() +
new com.android.framework.multidexlegacytestservices.manymethods.Big003().get3() +
new com.android.framework.multidexlegacytestservices.manymethods.Big004().get4() +

View File

@@ -0,0 +1,33 @@
# Copyright (C) 2014 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.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := MultiDexLegacyTestServicesTests2
LOCAL_JAVA_LIBRARIES := android-support-multidex
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
LOCAL_SDK_VERSION := 9
LOCAL_DEX_PREOPT := false
include $(BUILD_PACKAGE)

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.framework.multidexlegacytestservices.test2"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="9" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
<instrumentation
android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.framework.multidexlegacytestservices" />
<application
android:label="multidexlegacytestservices.test2" >
<uses-library android:name="android.test.runner" />
</application>
</manifest>

View File

@@ -0,0 +1,381 @@
/*
* Copyright (C) 2018 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 com.android.framework.multidexlegacytestservices.test2;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.TimeoutException;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Run the tests with: <code>adb shell am instrument -w
* com.android.framework.multidexlegacytestservices.test2/android.support.test.runner.AndroidJUnitRunner
* </code>
*/
@RunWith(AndroidJUnit4.class)
public class ServicesTests {
private static final String TAG = "ServicesTests";
static {
Log.i(TAG, "Initializing");
}
private class ExtensionFilter implements FileFilter {
private final String ext;
public ExtensionFilter(String ext) {
this.ext = ext;
}
@Override
public boolean accept(File file) {
return file.getName().endsWith(ext);
}
}
private class ExtractedZipFilter extends ExtensionFilter {
public ExtractedZipFilter() {
super(".zip");
}
@Override
public boolean accept(File file) {
return super.accept(file) && !file.getName().startsWith("tmp-");
}
}
private static final int ENDHDR = 22;
private static final String SERVICE_BASE_ACTION =
"com.android.framework.multidexlegacytestservices.action.Service";
private static final int MIN_SERVICE = 1;
private static final int MAX_SERVICE = 19;
private static final String COMPLETION_SUCCESS = "Success";
private File targetFilesDir;
@Before
public void setup() throws Exception {
Log.i(TAG, "setup");
killServices();
File applicationDataDir =
new File(InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir);
clearDirContent(applicationDataDir);
targetFilesDir = InstrumentationRegistry.getTargetContext().getFilesDir();
Log.i(TAG, "setup done");
}
@Test
public void testStressConcurentLaunch() throws Exception {
startServices();
waitServicesCompletion();
String completionStatus = getServicesCompletionStatus();
if (completionStatus != COMPLETION_SUCCESS) {
Assert.fail(completionStatus);
}
}
@Test
public void testRecoverFromZipCorruption() throws Exception {
int serviceId = 1;
// Ensure extraction.
initServicesWorkFiles();
startService(serviceId);
waitServicesCompletion(serviceId);
// Corruption of the extracted zips.
tamperAllExtractedZips();
killServices();
checkRecover();
}
@Test
public void testRecoverFromDexCorruption() throws Exception {
int serviceId = 1;
// Ensure extraction.
initServicesWorkFiles();
startService(serviceId);
waitServicesCompletion(serviceId);
// Corruption of the odex files.
tamperAllOdex();
killServices();
checkRecover();
}
@Test
public void testRecoverFromZipCorruptionStressTest() throws Exception {
Thread startServices =
new Thread() {
@Override
public void run() {
startServices();
}
};
startServices.start();
// Start services lasts more than 80s, lets cause a few corruptions during this interval.
for (int i = 0; i < 80; i++) {
Thread.sleep(1000);
tamperAllExtractedZips();
}
startServices.join();
try {
waitServicesCompletion();
} catch (TimeoutException e) {
// Can happen.
}
killServices();
checkRecover();
}
@Test
public void testRecoverFromDexCorruptionStressTest() throws Exception {
Thread startServices =
new Thread() {
@Override
public void run() {
startServices();
}
};
startServices.start();
// Start services lasts more than 80s, lets cause a few corruptions during this interval.
for (int i = 0; i < 80; i++) {
Thread.sleep(1000);
tamperAllOdex();
}
startServices.join();
try {
waitServicesCompletion();
} catch (TimeoutException e) {
// Will probably happen most of the time considering what we're doing...
}
killServices();
checkRecover();
}
private static void clearDirContent(File dir) {
for (File subElement : dir.listFiles()) {
if (subElement.isDirectory()) {
clearDirContent(subElement);
}
if (!subElement.delete()) {
throw new AssertionError("Failed to clear '" + subElement.getAbsolutePath() + "'");
}
}
}
private void startServices() {
Log.i(TAG, "start services");
initServicesWorkFiles();
for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
startService(i);
try {
Thread.sleep((i - 1) * (1 << (i / 5)));
} catch (InterruptedException e) {
}
}
}
private void startService(int serviceId) {
Log.i(TAG, "start service " + serviceId);
InstrumentationRegistry.getContext().startService(new Intent(SERVICE_BASE_ACTION + serviceId));
}
private void initServicesWorkFiles() {
for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
File resultFile = new File(targetFilesDir, "Service" + i);
resultFile.delete();
Assert.assertFalse(
"Failed to delete result file '" + resultFile.getAbsolutePath() + "'.",
resultFile.exists());
File completeFile = new File(targetFilesDir, "Service" + i + ".complete");
completeFile.delete();
Assert.assertFalse(
"Failed to delete completion file '" + completeFile.getAbsolutePath() + "'.",
completeFile.exists());
}
}
private void waitServicesCompletion() throws TimeoutException {
Log.i(TAG, "start sleeping");
int attempt = 0;
int maxAttempt = 50; // 10 is enough for a nexus S
do {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
attempt++;
if (attempt >= maxAttempt) {
throw new TimeoutException();
}
} while (!areAllServicesCompleted());
}
private void waitServicesCompletion(int serviceId) throws TimeoutException {
Log.i(TAG, "start sleeping");
int attempt = 0;
int maxAttempt = 50; // 10 is enough for a nexus S
do {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
attempt++;
if (attempt >= maxAttempt) {
throw new TimeoutException();
}
} while (isServiceRunning(serviceId));
}
private String getServicesCompletionStatus() {
String status = COMPLETION_SUCCESS;
for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
File resultFile = new File(targetFilesDir, "Service" + i);
if (!resultFile.isFile()) {
status = "Service" + i + " never completed.";
break;
}
if (resultFile.length() != 8) {
status = "Service" + i + " was restarted.";
break;
}
}
Log.i(TAG, "Services completion status: " + status);
return status;
}
private String getServiceCompletionStatus(int serviceId) {
String status = COMPLETION_SUCCESS;
File resultFile = new File(targetFilesDir, "Service" + serviceId);
if (!resultFile.isFile()) {
status = "Service" + serviceId + " never completed.";
} else if (resultFile.length() != 8) {
status = "Service" + serviceId + " was restarted.";
}
Log.i(TAG, "Service " + serviceId + " completion status: " + status);
return status;
}
private boolean areAllServicesCompleted() {
for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
if (isServiceRunning(i)) {
return false;
}
}
return true;
}
private boolean isServiceRunning(int i) {
File completeFile = new File(targetFilesDir, "Service" + i + ".complete");
return !completeFile.exists();
}
private File getSecondaryFolder() {
File dir =
new File(
new File(
InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir,
"code_cache"),
"secondary-dexes");
Assert.assertTrue(dir.getAbsolutePath(), dir.isDirectory());
return dir;
}
private void tamperAllExtractedZips() throws IOException {
// First attempt was to just overwrite zip entries but keep central directory, this was no
// trouble for Dalvik that was just ignoring those zip and using the odex files.
Log.i(TAG, "Tamper extracted zip files by overwriting all content by '\\0's.");
byte[] zeros = new byte[4 * 1024];
// Do not tamper tmp zip during their extraction.
for (File zip : getSecondaryFolder().listFiles(new ExtractedZipFilter())) {
long fileLength = zip.length();
Assert.assertTrue(fileLength > ENDHDR);
zip.setWritable(true);
RandomAccessFile raf = new RandomAccessFile(zip, "rw");
try {
int index = 0;
while (index < fileLength) {
int length = (int) Math.min(zeros.length, fileLength - index);
raf.write(zeros, 0, length);
index += length;
}
} finally {
raf.close();
}
}
}
private void tamperAllOdex() throws IOException {
Log.i(TAG, "Tamper odex files by overwriting some content by '\\0's.");
byte[] zeros = new byte[4 * 1024];
// I think max size would be 40 (u1[8] + 8 u4) but it's a test so lets take big margins.
int savedSizeForOdexHeader = 80;
for (File odex : getSecondaryFolder().listFiles(new ExtensionFilter(".dex"))) {
long fileLength = odex.length();
Assert.assertTrue(fileLength > zeros.length + savedSizeForOdexHeader);
odex.setWritable(true);
RandomAccessFile raf = new RandomAccessFile(odex, "rw");
try {
raf.seek(savedSizeForOdexHeader);
raf.write(zeros, 0, zeros.length);
} finally {
raf.close();
}
}
}
private void checkRecover() throws TimeoutException {
Log.i(TAG, "Check recover capability");
int serviceId = 1;
// Start one service and check it was able to run correctly even if a previous run failed.
initServicesWorkFiles();
startService(serviceId);
waitServicesCompletion(serviceId);
String completionStatus = getServiceCompletionStatus(serviceId);
if (completionStatus != COMPLETION_SUCCESS) {
Assert.fail(completionStatus);
}
}
private void killServices() {
((ActivityManager)
InstrumentationRegistry.getContext().getSystemService(Context.ACTIVITY_SERVICE))
.killBackgroundProcesses("com.android.framework.multidexlegacytestservices");
}
}