Merge "Added crash-detection mechanism."
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/CrashedDummyResult.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/CrashedDummyResult.java
new file mode 100644
index 0000000..a793dab
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/CrashedDummyResult.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010 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.dumprendertree2;
+
+import android.os.Bundle;
+import android.os.Message;
+import android.webkit.WebView;
+
+/**
+ * A dummy class representing test that crashed.
+ */
+public class CrashedDummyResult extends AbstractResult {
+    String mRelativePath;
+
+    public CrashedDummyResult(String relativePath) {
+        mRelativePath = relativePath;
+    }
+
+    @Override
+    public byte[] getActualImageResult() {
+        return null;
+    }
+
+    @Override
+    public String getActualTextResult() {
+        return null;
+    }
+
+    @Override
+    public Bundle getBundle() {
+        /** TODO:  */
+        return null;
+    }
+
+    @Override
+    public String getDiffAsHtml() {
+        /** TODO: Probably show at least expected results */
+        return "Ooops, I crashed...";
+    }
+
+    @Override
+    public String getRelativePath() {
+        return mRelativePath;
+    }
+
+    @Override
+    public ResultCode getResultCode() {
+        return ResultCode.FAIL_CRASHED;
+    }
+
+    @Override
+    public TestType getType() {
+        return null;
+    }
+
+    @Override
+    public void obtainActualResults(WebView webview, Message resultObtainedMsg) {
+        /** This method is not applicable for this type of result */
+        assert false;
+    }
+
+    @Override
+    public void setExpectedImageResult(byte[] expectedResult) {
+        /** TODO */
+    }
+
+    @Override
+    public void setExpectedTextResult(String expectedResult) {
+        /** TODO */
+    }
+}
\ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java
index 063cb73..dc82c34 100644
--- a/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java
@@ -82,6 +82,7 @@
 
     private static final int DEFAULT_TIME_OUT_MS = 15 * 1000;
 
+    /** A list of tests that remain to run since last crash */
     private List<String> mTestsList;
 
     /**
@@ -91,6 +92,7 @@
      */
     private int mCurrentTestIndex;
 
+    /** The total number of tests to run, doesn't reset after crash */
     private int mTotalTestCount;
 
     private WebView mCurrentWebView;
@@ -119,7 +121,7 @@
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             mManagerServiceMessenger = new Messenger(service);
-            runNextTest();
+            startTests();
         }
 
         @Override
@@ -303,6 +305,26 @@
         webViewSettings.setXSSAuditorEnabled(false);
     }
 
+    private void startTests() {
+        try {
+            Message serviceMsg =
+                    Message.obtain(null, ManagerService.MSG_FIRST_TEST);
+
+            Bundle bundle = new Bundle();
+            if (!mTestsList.isEmpty()) {
+                bundle.putString("firstTest", mTestsList.get(0));
+                bundle.putInt("index", mCurrentTestIndex);
+            }
+
+            serviceMsg.setData(bundle);
+            mManagerServiceMessenger.send(serviceMsg);
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG + "::startTests", e.getMessage());
+        }
+
+        runNextTest();
+    }
+
     private void runNextTest() {
         assert mCurrentState == CurrentState.IDLE : "mCurrentState = " + mCurrentState.name();
 
@@ -312,6 +334,8 @@
         }
 
         mCurrentTestRelativePath = mTestsList.remove(0);
+        Log.d(LOG_TAG + "::runNextTest", "Start: " + mCurrentTestRelativePath +
+                "(" + mCurrentTestIndex + ")");
         mCurrentTestUri =
                 Uri.fromFile(new File(TESTS_ROOT_DIR_PATH, mCurrentTestRelativePath)).toString();
 
@@ -386,6 +410,9 @@
             if (mCurrentTestTimedOut) {
                 bundle.putString("resultCode", AbstractResult.ResultCode.FAIL_TIMED_OUT.name());
             }
+            if (!mTestsList.isEmpty()) {
+                bundle.putString("nextTest", mTestsList.get(0));
+            }
 
             serviceMsg.setData(bundle);
             mManagerServiceMessenger.send(serviceMsg);
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java
index 3c77a55..3bbfb89 100644
--- a/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java
@@ -38,6 +38,10 @@
 
     private static final String LOG_TAG = "ManagerService";
 
+    private static final int MSG_TEST_CRASHED = 0;
+
+    private static final int CRASH_TIMEOUT_MS = 20 * 1000;
+
     /** TODO: make it a setting */
     static final String TESTS_ROOT_DIR_PATH =
             Environment.getExternalStorageDirectory() +
@@ -67,11 +71,21 @@
 
     static final int MSG_PROCESS_ACTUAL_RESULTS = 0;
     static final int MSG_ALL_TESTS_FINISHED = 1;
+    static final int MSG_FIRST_TEST = 2;
 
+    /**
+     * This handler is purely for IPC. It is used to create mMessenger
+     * that generates a binder returned in onBind method.
+     */
     private Handler mIncomingHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
+                case MSG_FIRST_TEST:
+                    Bundle bundle = msg.getData();
+                    ensureNextTestSetup(bundle.getString("firstTest"), bundle.getInt("index"));
+                    break;
+
                 case MSG_PROCESS_ACTUAL_RESULTS:
                     Log.d(LOG_TAG + ".mIncomingHandler", msg.getData().getString("relativePath"));
                     onActualResultsObtained(msg.getData());
@@ -86,9 +100,21 @@
 
     private Messenger mMessenger = new Messenger(mIncomingHandler);
 
+    private Handler mCrashMessagesHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_TEST_CRASHED) {
+                onTestCrashed();
+            }
+        }
+    };
+
     private FileFilter mFileFilter;
     private Summarizer mSummarizer;
 
+    private String mCurrentlyRunningTest;
+    private int mCurrentlyRunningTestIndex;
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -98,13 +124,54 @@
     }
 
     @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        return START_STICKY;
+    }
+
+    @Override
     public IBinder onBind(Intent intent) {
         return mMessenger.getBinder();
     }
 
     private void onActualResultsObtained(Bundle bundle) {
+        mCrashMessagesHandler.removeMessages(MSG_TEST_CRASHED);
+        ensureNextTestSetup(bundle.getString("nextTest"), bundle.getInt("testIndex") + 1);
+
         AbstractResult results =
                 AbstractResult.TestType.valueOf(bundle.getString("type")).createResult(bundle);
+
+        handleResults(results);
+    }
+
+    private void ensureNextTestSetup(String nextTest, int index) {
+        if (nextTest == null) {
+            return;
+        }
+
+        mCurrentlyRunningTest = nextTest;
+        mCurrentlyRunningTestIndex = index;
+        mCrashMessagesHandler.sendEmptyMessageDelayed(MSG_TEST_CRASHED, CRASH_TIMEOUT_MS);
+    }
+
+    /**
+     * This sends an intent to TestsListActivity to restart LayoutTestsExecutor.
+     * The more detailed description of the flow is in the comment of onNewIntent
+     * method in TestsListActivity.
+     */
+    private void onTestCrashed() {
+        handleResults(new CrashedDummyResult(mCurrentlyRunningTest));
+
+        Log.w(LOG_TAG + "::onTestCrashed", mCurrentlyRunningTest +
+                "(" + mCurrentlyRunningTestIndex + ")");
+
+        Intent intent = new Intent(this, TestsListActivity.class);
+        intent.setAction(Intent.ACTION_REBOOT);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        intent.putExtra("crashedTestIndex", mCurrentlyRunningTestIndex);
+        startActivity(intent);
+    }
+
+    private void handleResults(AbstractResult results) {
         String relativePath = results.getRelativePath();
         results.setExpectedTextResult(getExpectedTextResult(relativePath));
         results.setExpectedImageResult(getExpectedImageResult(relativePath));
@@ -134,7 +201,8 @@
             return;
         }
 
-        String resultPath = FileFilter.setPathEnding(testPath, "-actual." + IMAGE_RESULT_EXTENSION);
+        String resultPath = FileFilter.setPathEnding(testPath,
+                "-actual." + IMAGE_RESULT_EXTENSION);
         FsUtils.writeDataToStorage(new File(RESULTS_ROOT_DIR_PATH, resultPath),
                 actualImageResult, false);
     }
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java
index a402ae1..c21463b 100644
--- a/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java
@@ -29,7 +29,7 @@
 /**
  * An Activity that generates a list of tests and sends the intent to
  * LayoutTestsExecuter to run them. It also restarts the LayoutTestsExecuter
- * after it crashes (TODO).
+ * after it crashes.
  */
 public class TestsListActivity extends Activity {
 
@@ -79,10 +79,50 @@
         sProgressDialog.show();
         Message doneMsg = Message.obtain(mHandler, MSG_TEST_LIST_PRELOADER_DONE);
 
+        Intent serviceIntent = new Intent(this, ManagerService.class);
+        startService(serviceIntent);
+
         new TestsListPreloaderThread(path, doneMsg).start();
     }
 
     /**
+     * This method handles an intent that comes from ManageService when crash is detected.
+     * The intent contains an index in mTestsList of the test that crashed. TestsListActivity
+     * restarts the LayoutTestsExecutor from the following test in mTestsList, by sending
+     * an intent to it. This new intent contains a list of remaining tests to run,
+     * total count of all tests, and the index of the first test to run after restarting.
+     * LayoutTestExecutor runs then as usual, sending reports to ManagerService. If it
+     * detects the crash it sends a new intent and the flow repeats.
+     */
+    @Override
+    protected void onNewIntent(Intent intent) {
+        if (!intent.getAction().equals(Intent.ACTION_REBOOT)) {
+            return;
+        }
+
+        int nextTestToRun = intent.getIntExtra("crashedTestIndex", -1) + 1;
+        if (nextTestToRun > 0 && nextTestToRun <= mTotalTestCount) {
+            restartExecutor(nextTestToRun);
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        outState.putStringArrayList("testsList", mTestsList);
+        outState.putInt("totalCount", mTotalTestCount);
+
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+
+        mTestsList = savedInstanceState.getStringArrayList("testsList");
+        mTotalTestCount = savedInstanceState.getInt("totalCount");
+    }
+
+    /**
      * (Re)starts the executer activity from the given test number (inclusive, 0-based).
      * This number is an index in mTestsList, not the sublist passed in the intent.
      *
@@ -93,9 +133,16 @@
         Intent intent = new Intent();
         intent.setClass(this, LayoutTestsExecutor.class);
         intent.setAction(Intent.ACTION_RUN);
-        intent.putStringArrayListExtra(LayoutTestsExecutor.EXTRA_TESTS_LIST,
-                new ArrayList<String>(mTestsList.subList(startFrom, mTotalTestCount)));
-        intent.putExtra(LayoutTestsExecutor.EXTRA_TEST_INDEX, startFrom);
+
+        if (startFrom < mTotalTestCount) {
+            intent.putStringArrayListExtra(LayoutTestsExecutor.EXTRA_TESTS_LIST,
+                    new ArrayList<String>(mTestsList.subList(startFrom, mTotalTestCount)));
+            intent.putExtra(LayoutTestsExecutor.EXTRA_TEST_INDEX, startFrom);
+        } else {
+            intent.putStringArrayListExtra(LayoutTestsExecutor.EXTRA_TESTS_LIST,
+                    new ArrayList<String>());
+        }
+
         startActivity(intent);
     }
 }
\ No newline at end of file