summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Yohann Roussel <yroussel@google.com> 2018-01-15 14:27:00 +0100
committer Yohann Roussel <yroussel@google.com> 2018-01-17 15:42:52 +0100
commit493cebd02538adf347413450238bb1f3f7a0d541 (patch)
treea8e934011c70d34d98fbb32ae69893657ffde0f8
parente61ee4198e3e568ad0836e33e27bb4bdcb64b9f1 (diff)
Add tests about MultiDex corruption recovering
Those are testing extracted zip corruption and also corruption of their corresponding odex file and the capacity of MultiDex.install to restore a runnable state. Bug: 28832787 Test: This is the test Change-Id: I8dd99172d545e700b12c2a2b1391ef1aeb5560ce
-rw-r--r--core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk2
-rw-r--r--core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml4
-rw-r--r--core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java52
-rw-r--r--core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk33
-rw-r--r--core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml18
-rw-r--r--core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java381
6 files changed, 464 insertions, 26 deletions
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk
index 99bcd6c62b56..a6c537373f26 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk
@@ -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
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml
index e30689203c1b..7cd01e54a64e 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml
@@ -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>
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java
index 7b83999d0ca9..cb0a591559db 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java
@@ -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() +
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk
new file mode 100644
index 000000000000..f3d98a88d485
--- /dev/null
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk
@@ -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)
+
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
new file mode 100644
index 000000000000..0ab29591be18
--- /dev/null
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
@@ -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> \ No newline at end of file
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java
new file mode 100644
index 000000000000..900f20387c49
--- /dev/null
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java
@@ -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");
+ }
+}