summaryrefslogtreecommitdiffstats
path: root/tests/DumpRenderTree2
diff options
context:
space:
mode:
Diffstat (limited to 'tests/DumpRenderTree2')
-rw-r--r--tests/DumpRenderTree2/Android.mk29
-rw-r--r--tests/DumpRenderTree2/AndroidManifest.xml60
-rwxr-xr-xtests/DumpRenderTree2/assets/run_apache2.py150
-rwxr-xr-xtests/DumpRenderTree2/assets/run_layout_tests.py94
-rw-r--r--tests/DumpRenderTree2/res/drawable/folder.pngbin0 -> 4865 bytes
-rw-r--r--tests/DumpRenderTree2/res/drawable/runtest.pngbin0 -> 3146 bytes
-rw-r--r--tests/DumpRenderTree2/res/layout/dirlist_row.xml43
-rw-r--r--tests/DumpRenderTree2/res/menu/gui_menu.xml20
-rw-r--r--tests/DumpRenderTree2/res/values/strings.xml30
-rw-r--r--tests/DumpRenderTree2/res/values/style.xml21
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/AbstractResult.java249
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/AdditionalTextOutput.java119
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/CrashedDummyResult.java125
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSender.java110
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSenderImpl.java580
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/FileFilter.java303
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/FsUtils.java250
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestController.java116
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java729
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java291
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/Summarizer.java573
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/SummarizerDBHelper.java129
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java202
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListPreloaderThread.java114
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/TextResult.java256
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/VisualDiffUtils.java214
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/AdbUtils.java75
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ConnectionHandler.java172
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/Forwarder.java132
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ForwarderManager.java123
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/OnEverythingFinishedCallback.java25
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/ScriptTestRunner.java37
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/Starter.java79
-rw-r--r--tests/DumpRenderTree2/src/com/android/dumprendertree2/ui/DirListActivity.java426
34 files changed, 5876 insertions, 0 deletions
diff --git a/tests/DumpRenderTree2/Android.mk b/tests/DumpRenderTree2/Android.mk
new file mode 100644
index 0000000..81fc633
--- /dev/null
+++ b/tests/DumpRenderTree2/Android.mk
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := diff_match_patch
+
+LOCAL_PACKAGE_NAME := DumpRenderTree2
+
+include $(BUILD_PACKAGE) \ No newline at end of file
diff --git a/tests/DumpRenderTree2/AndroidManifest.xml b/tests/DumpRenderTree2/AndroidManifest.xml
new file mode 100644
index 0000000..ea6571e
--- /dev/null
+++ b/tests/DumpRenderTree2/AndroidManifest.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.dumprendertree2">
+ <application>
+ <uses-library android:name="android.test.runner" />
+
+ <activity android:name=".ui.DirListActivity"
+ android:label="Dump Render Tree 2"
+ android:configChanges="orientation">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.TEST" />
+ </intent-filter>
+ </activity>
+
+ <!-- android:launchMode="singleTask" is there so we only have a one instance
+ of this activity. However, it doesn't seem to work exactly like described in the
+ documentation, because the behaviour of the application suggest
+ there is only a single task for all 3 activities. We don't understand
+ how exactly it all works, but at the moment it works just fine.
+ It can lead to some weird behaviour in the future. -->
+ <activity android:name=".TestsListActivity"
+ android:label="Tests' list activity"
+ android:launchMode="singleTask"
+ android:configChanges="orientation">
+ </activity>
+
+ <activity android:name=".LayoutTestsExecutor"
+ android:theme="@style/WhiteBackground"
+ android:label="Layout tests' executor"
+ android:process=":executor">
+ </activity>
+
+ <service android:name="ManagerService">
+ </service>
+ </application>
+
+ <instrumentation android:name="com.android.dumprendertree2.scriptsupport.ScriptTestRunner"
+ android:targetPackage="com.android.dumprendertree2"
+ android:label="Layout tests script runner" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WRITE_SDCARD" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+</manifest>
diff --git a/tests/DumpRenderTree2/assets/run_apache2.py b/tests/DumpRenderTree2/assets/run_apache2.py
new file mode 100755
index 0000000..5edead1
--- /dev/null
+++ b/tests/DumpRenderTree2/assets/run_apache2.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+#
+# 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.
+#
+"""Start, stop, or restart apache2 server.
+
+ Apache2 must be installed with mod_php!
+
+ Usage:
+ run_apache2.py start|stop|restart
+"""
+
+import sys
+import os
+import subprocess
+import logging
+import optparse
+import time
+
+def main(options, args):
+ if len(args) < 1:
+ run_cmd = ""
+ else:
+ run_cmd = args[0]
+
+ # Setup logging class
+ logging.basicConfig(level=logging.INFO, format='%(message)s')
+
+ if not run_cmd in ("start", "stop", "restart"):
+ logging.info("illegal argument: " + run_cmd)
+ logging.info("Usage: python run_apache2.py start|stop|restart")
+ return
+
+ # Create /tmp/WebKit if it doesn't exist. This is needed for various files used by apache2
+ tmp_WebKit = os.path.join("/tmp", "WebKit")
+ if not os.path.exists(tmp_WebKit):
+ os.mkdir(tmp_WebKit)
+
+ # Get the path to android tree root based on the script location.
+ # Basically we go 5 levels up
+ parent = os.pardir
+ script_location = os.path.abspath(os.path.dirname(sys.argv[0]))
+ android_tree_root = os.path.join(script_location, parent, parent, parent, parent, parent)
+ android_tree_root = os.path.normpath(android_tree_root)
+
+ # If any of these is relative, then it's relative to ServerRoot (in our case android_tree_root)
+ webkit_path = os.path.join("external", "webkit")
+ if (options.tests_root_directory != None):
+ # if options.tests_root_directory is absolute, os.getcwd() is discarded!
+ layout_tests_path = os.path.normpath(os.path.join(os.getcwd(), options.tests_root_directory))
+ else:
+ layout_tests_path = os.path.join(webkit_path, "LayoutTests")
+ http_conf_path = os.path.join(layout_tests_path, "http", "conf")
+
+ # Prepare the command to set ${APACHE_RUN_USER} and ${APACHE_RUN_GROUP}
+ envvars_path = os.path.join("/etc", "apache2", "envvars")
+ export_envvars_cmd = "source " + envvars_path
+
+ error_log_path = os.path.join(tmp_WebKit, "apache2-error.log")
+ custom_log_path = os.path.join(tmp_WebKit, "apache2-access.log")
+
+ # Prepare the command to (re)start/stop the server with specified settings
+ apache2_restart_template = "apache2 -k %s"
+ directives = " -c \"ServerRoot " + android_tree_root + "\""
+
+ # The default config in apache2-debian-httpd.conf listens on ports 8080 and
+ # 8443. We also need to listen on port 8000 for HTTP tests.
+ directives += " -c \"Listen 8000\""
+
+ # We use http/tests as the document root as the HTTP tests use hardcoded
+ # resources at the server root. We then use aliases to make available the
+ # complete set of tests and the required scripts.
+ directives += " -c \"DocumentRoot " + os.path.join(layout_tests_path, "http", "tests/") + "\""
+ directives += " -c \"Alias /LayoutTests " + layout_tests_path + "\""
+ directives += " -c \"Alias /WebKitTools/DumpRenderTree/android " + \
+ os.path.join(webkit_path, "WebKitTools", "DumpRenderTree", "android") + "\""
+ directives += " -c \"Alias /ThirdPartyProject.prop " + \
+ os.path.join(webkit_path, "ThirdPartyProject.prop") + "\""
+
+ # This directive is commented out in apache2-debian-httpd.conf for some reason
+ # However, it is useful to browse through tests in the browser, so it's added here.
+ # One thing to note is that because of problems with mod_dir and port numbers, mod_dir
+ # is turned off. That means that there _must_ be a trailing slash at the end of URL
+ # for auto indexes to work correctly.
+ directives += " -c \"LoadModule autoindex_module /usr/lib/apache2/modules/mod_autoindex.so\""
+
+ directives += " -c \"ErrorLog " + error_log_path +"\""
+ directives += " -c \"CustomLog " + custom_log_path + " combined\""
+ directives += " -c \"SSLCertificateFile " + os.path.join(http_conf_path, "webkit-httpd.pem") + \
+ "\""
+ directives += " -c \"User ${APACHE_RUN_USER}\""
+ directives += " -c \"Group ${APACHE_RUN_GROUP}\""
+ directives += " -C \"TypesConfig " + \
+ os.path.join(android_tree_root, http_conf_path, "mime.types") + "\""
+ conf_file_cmd = " -f " + \
+ os.path.join(android_tree_root, http_conf_path, "apache2-debian-httpd.conf")
+
+ # Try to execute the commands
+ logging.info("Will " + run_cmd + " apache2 server.")
+
+ # It is worth noting here that if the configuration file with which we restart the server points
+ # to a different PidFile it will not work and will result in a second apache2 instance.
+ if (run_cmd == 'restart'):
+ logging.info("First will stop...")
+ execute_cmd(export_envvars_cmd + " && " + (apache2_restart_template % ('stop')) + directives + conf_file_cmd)
+ logging.info("Stopped. Will start now...")
+ # We need to sleep breifly to avoid errors with apache being stopped and started too quickly
+ time.sleep(0.5)
+
+ execute_cmd(export_envvars_cmd + " && " + (apache2_restart_template % (run_cmd)) + directives + conf_file_cmd)
+
+def execute_cmd(cmd):
+ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (out, err) = p.communicate()
+
+ # Output the stdout from the command to console
+ logging.info(out)
+
+ # Report any errors
+ if p.returncode != 0:
+ logging.info("!! ERRORS:")
+
+ if err.find(envvars_path) != -1:
+ logging.info(err)
+ elif err.find('command not found') != -1:
+ logging.info("apache2 is probably not installed")
+ else:
+ logging.info(err)
+ logging.info("Try looking in " + error_log_path + " for details")
+ else:
+ logging.info("OK")
+
+if __name__ == "__main__":
+ option_parser = optparse.OptionParser(usage="Usage: %prog [options] start|stop|restart")
+ option_parser.add_option("", "--tests-root-directory",
+ help="The directory from which to take the tests, default is external/webkit/LayoutTests in this checkout of the Android tree")
+ options, args = option_parser.parse_args();
+ main(options, args);
diff --git a/tests/DumpRenderTree2/assets/run_layout_tests.py b/tests/DumpRenderTree2/assets/run_layout_tests.py
new file mode 100755
index 0000000..303a054
--- /dev/null
+++ b/tests/DumpRenderTree2/assets/run_layout_tests.py
@@ -0,0 +1,94 @@
+#!/usr/bin/python
+
+"""Run layout tests on the device.
+
+ It runs the specified tests on the device, downloads the summaries to the temporary directory
+ and optionally shows the detailed results the host's default browser.
+
+ Usage:
+ run_layout_tests.py --show-results-in-browser test-relative-path
+"""
+
+import logging
+import optparse
+import os
+import re
+import sys
+import subprocess
+import tempfile
+import webbrowser
+
+#TODO: These should not be hardcoded
+RESULTS_ABSOLUTE_PATH = "/sdcard/layout-test-results/"
+DETAILS_HTML = "details.html"
+SUMMARY_TXT = "summary.txt"
+
+def main(options, args):
+ if args:
+ path = " ".join(args);
+ else:
+ path = "";
+
+ logging.basicConfig(level=logging.INFO, format='%(message)s')
+
+ tmpdir = tempfile.gettempdir()
+
+ if options.tests_root_directory != None:
+ # if options.tests_root_directory is absolute, os.getcwd() is discarded!
+ tests_root_directory = os.path.normpath(os.path.join(os.getcwd(), options.tests_root_directory))
+ server_options = " --tests-root-directory=" + tests_root_directory
+ else:
+ server_options = "";
+
+ # Restart the server
+ cmd = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "run_apache2.py") + server_options + " restart"
+ os.system(cmd);
+
+ # Run the tests in path
+ adb_cmd = "adb"
+ if options.serial:
+ adb_cmd += " -s " + options.serial
+ cmd = adb_cmd + " shell am instrument "
+ cmd += "-e class com.android.dumprendertree2.scriptsupport.Starter#startLayoutTests "
+ cmd += "-e path \"" + path + "\" "
+ cmd += "-w com.android.dumprendertree2/com.android.dumprendertree2.scriptsupport.ScriptTestRunner"
+
+ logging.info("Running the tests...")
+ (stdoutdata, stderrdata) = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+ if re.search("^INSTRUMENTATION_STATUS_CODE: -1", stdoutdata, re.MULTILINE) != None:
+ logging.info("Failed to run the tests. Is DumpRenderTree2 installed on the device?")
+ return
+
+ logging.info("Downloading the summaries...")
+
+ # Download the txt summary to tmp folder
+ summary_txt_tmp_path = os.path.join(tmpdir, SUMMARY_TXT)
+ cmd = "rm -f " + summary_txt_tmp_path + ";"
+ cmd += adb_cmd + " pull " + RESULTS_ABSOLUTE_PATH + SUMMARY_TXT + " " + summary_txt_tmp_path
+ subprocess.Popen(cmd, shell=True).wait()
+
+ # Download the html summary to tmp folder
+ details_html_tmp_path = os.path.join(tmpdir, DETAILS_HTML)
+ cmd = "rm -f " + details_html_tmp_path + ";"
+ cmd += adb_cmd + " pull " + RESULTS_ABSOLUTE_PATH + DETAILS_HTML + " " + details_html_tmp_path
+ subprocess.Popen(cmd, shell=True).wait()
+
+ # Print summary to console
+ logging.info("All done.\n")
+ cmd = "cat " + summary_txt_tmp_path
+ os.system(cmd)
+ logging.info("")
+
+ # Open the browser with summary
+ if options.show_results_in_browser != "false":
+ webbrowser.open(details_html_tmp_path)
+
+if __name__ == "__main__":
+ option_parser = optparse.OptionParser(usage="Usage: %prog [options] test-relative-path")
+ option_parser.add_option("", "--show-results-in-browser", default="true",
+ help="Show the results the host's default web browser, default=true")
+ option_parser.add_option("", "--tests-root-directory",
+ help="The directory from which to take the tests, default is external/webkit/LayoutTests in this checkout of the Android tree")
+ option_parser.add_option("-s", "--serial", default=None, help="Specify the serial number of device to run test on")
+ options, args = option_parser.parse_args();
+ main(options, args);
diff --git a/tests/DumpRenderTree2/res/drawable/folder.png b/tests/DumpRenderTree2/res/drawable/folder.png
new file mode 100644
index 0000000..5b3fcec
--- /dev/null
+++ b/tests/DumpRenderTree2/res/drawable/folder.png
Binary files differ
diff --git a/tests/DumpRenderTree2/res/drawable/runtest.png b/tests/DumpRenderTree2/res/drawable/runtest.png
new file mode 100644
index 0000000..910c654
--- /dev/null
+++ b/tests/DumpRenderTree2/res/drawable/runtest.png
Binary files differ
diff --git a/tests/DumpRenderTree2/res/layout/dirlist_row.xml b/tests/DumpRenderTree2/res/layout/dirlist_row.xml
new file mode 100644
index 0000000..e5578a6
--- /dev/null
+++ b/tests/DumpRenderTree2/res/layout/dirlist_row.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="80px"
+ android:adjustViewBounds="true"
+ android:paddingLeft="15px"
+ android:paddingRight="15px"
+ android:paddingTop="15px"
+ android:paddingBottom="15px"
+ android:layout_height="wrap_content"
+ />
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="60px"
+ android:gravity="center_vertical"
+ android:textSize="14sp"
+ />
+
+</LinearLayout> \ No newline at end of file
diff --git a/tests/DumpRenderTree2/res/menu/gui_menu.xml b/tests/DumpRenderTree2/res/menu/gui_menu.xml
new file mode 100644
index 0000000..a5b2b65
--- /dev/null
+++ b/tests/DumpRenderTree2/res/menu/gui_menu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/run_all"
+ android:title="@string/run_all_tests" />
+</menu> \ No newline at end of file
diff --git a/tests/DumpRenderTree2/res/values/strings.xml b/tests/DumpRenderTree2/res/values/strings.xml
new file mode 100644
index 0000000..0496404
--- /dev/null
+++ b/tests/DumpRenderTree2/res/values/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<resources>
+ <string name="dialog_run_abort_dir_title_prefix">Directory:</string>
+ <string name="dialog_run_abort_dir_msg">This will run all the tests in this directory and all
+ the subdirectories. It may take a few hours!</string>
+ <string name="dialog_run_abort_dir_ok_button">Run tests!</string>
+ <string name="dialog_run_abort_dir_abort_button">Abort</string>
+
+ <string name="dialog_progress_title">Loading items.</string>
+ <string name="dialog_progress_msg">Please wait...</string>
+
+ <string name="runner_preloading_title">Preloading tests...</string>
+
+ <string name="run_all_tests">Run all tests in the current directory</string>
+</resources> \ No newline at end of file
diff --git a/tests/DumpRenderTree2/res/values/style.xml b/tests/DumpRenderTree2/res/values/style.xml
new file mode 100644
index 0000000..35f3419
--- /dev/null
+++ b/tests/DumpRenderTree2/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<resources>
+ <style name="WhiteBackground">
+ <item name="android:background">@android:color/white</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/AbstractResult.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/AbstractResult.java
new file mode 100644
index 0000000..614b03c
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/AbstractResult.java
@@ -0,0 +1,249 @@
+/*
+ * 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.util.Log;
+import android.webkit.WebView;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/**
+ * A class that represent a result of the test. It is responsible for returning the result's
+ * raw data and generating its own diff in HTML format.
+ */
+public abstract class AbstractResult implements Comparable<AbstractResult>, Serializable {
+
+ private static final String LOG_TAG = "AbstractResult";
+
+ public enum TestType {
+ TEXT {
+ @Override
+ public AbstractResult createResult(Bundle bundle) {
+ return new TextResult(bundle);
+ }
+ },
+ RENDER_TREE {
+ @Override
+ public AbstractResult createResult(Bundle bundle) {
+ /** TODO: RenderTree tests are not yet supported */
+ return null;
+ }
+ };
+
+ public abstract AbstractResult createResult(Bundle bundle);
+ }
+
+ /**
+ * A code representing the result of comparing actual and expected results.
+ */
+ public enum ResultCode implements Serializable {
+ RESULTS_MATCH("Results match"),
+ RESULTS_DIFFER("Results differ"),
+ NO_EXPECTED_RESULT("No expected result"),
+ NO_ACTUAL_RESULT("No actual result");
+
+ private String mTitle;
+
+ private ResultCode(String title) {
+ mTitle = title;
+ }
+
+ @Override
+ public String toString() {
+ return mTitle;
+ }
+ }
+
+ String mAdditionalTextOutputString;
+
+ public int compareTo(AbstractResult another) {
+ return getRelativePath().compareTo(another.getRelativePath());
+ }
+
+ public void setAdditionalTextOutputString(String additionalTextOutputString) {
+ mAdditionalTextOutputString = additionalTextOutputString;
+ }
+
+ public String getAdditionalTextOutputString() {
+ return mAdditionalTextOutputString;
+ }
+
+ public byte[] getBytes() {
+ ByteArrayOutputStream baos = null;
+ ObjectOutputStream oos = null;
+ try {
+ try {
+ baos = new ByteArrayOutputStream();
+ oos = new ObjectOutputStream(baos);
+ oos.writeObject(this);
+ } finally {
+ if (baos != null) {
+ baos.close();
+ }
+ if (oos != null) {
+ oos.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unable to serialize result: " + getRelativePath(), e);
+ }
+
+ return baos == null ? null : baos.toByteArray();
+ }
+
+ public static AbstractResult create(byte[] bytes) {
+ ByteArrayInputStream bais = null;
+ ObjectInputStream ois = null;
+ AbstractResult result = null;
+ try {
+ try {
+ bais = new ByteArrayInputStream(bytes);
+ ois = new ObjectInputStream(bais);
+ result = (AbstractResult)ois.readObject();
+ } finally {
+ if (bais != null) {
+ bais.close();
+ }
+ if (ois != null) {
+ ois.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unable to deserialize result!", e);
+ } catch (ClassNotFoundException e) {
+ Log.e(LOG_TAG, "Unable to deserialize result!", e);
+ }
+ return result;
+ }
+
+ public void clearResults() {
+ mAdditionalTextOutputString = null;
+ }
+
+ /**
+ * Makes the result object obtain the results of the test from the webview
+ * and store them in the format that suits itself bests. This method is asynchronous.
+ * The message passed as a parameter is a message that should be sent to its target
+ * when the result finishes obtaining the result.
+ *
+ * @param webview
+ * @param resultObtainedMsg
+ */
+ public abstract void obtainActualResults(WebView webview, Message resultObtainedMsg);
+
+ public abstract void setExpectedImageResult(byte[] expectedResult);
+
+ public abstract void setExpectedImageResultPath(String relativePath);
+
+ public abstract String getExpectedImageResultPath();
+
+ public abstract void setExpectedTextResult(String expectedResult);
+
+ public abstract void setExpectedTextResultPath(String relativePath);
+
+ public abstract String getExpectedTextResultPath();
+
+ /**
+ * Returns result's image data that can be written to the disk. It can be null
+ * if there is an error of some sort or for example the test times out.
+ *
+ * <p> Some tests will not provide data (like text tests)
+ *
+ * @return
+ * results image data
+ */
+ public abstract byte[] getActualImageResult();
+
+ /**
+ * Returns result's text data. It can be null
+ * if there is an error of some sort or for example the test times out.
+ *
+ * @return
+ * results text data
+ */
+ public abstract String getActualTextResult();
+
+ /**
+ * Returns the status code representing the result of comparing actual and expected results.
+ *
+ * @return
+ * the status code from comparing actual and expected results
+ */
+ public abstract ResultCode getResultCode();
+
+ /**
+ * Returns whether this test crashed.
+ *
+ * @return
+ * whether this test crashed
+ */
+ public abstract boolean didCrash();
+
+ /**
+ * Returns whether this test timed out.
+ *
+ * @return
+ * whether this test timed out
+ */
+ public abstract boolean didTimeOut();
+
+ /**
+ * Sets that this test timed out.
+ */
+ public abstract void setDidTimeOut();
+
+ /**
+ * Returns whether the test passed.
+ *
+ * @return
+ * whether the test passed
+ */
+ public boolean didPass() {
+ // Tests that crash can't have timed out or have an actual result.
+ assert !(didCrash() && didTimeOut());
+ assert !(didCrash() && getResultCode() != ResultCode.NO_ACTUAL_RESULT);
+ return !didCrash() && !didTimeOut() && getResultCode() == ResultCode.RESULTS_MATCH;
+ }
+
+ /**
+ * Return the type of the result data.
+ *
+ * @return
+ * the type of the result data.
+ */
+ public abstract TestType getType();
+
+ public abstract String getRelativePath();
+
+ /**
+ * Returns a piece of HTML code that presents a visual diff between a result and
+ * the expected result.
+ *
+ * @return
+ * a piece of HTML code with a visual diff between the result and the expected result
+ */
+ public abstract String getDiffAsHtml();
+
+ public abstract Bundle getBundle();
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/AdditionalTextOutput.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/AdditionalTextOutput.java
new file mode 100644
index 0000000..bb9a916
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/AdditionalTextOutput.java
@@ -0,0 +1,119 @@
+/*
+ * 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.util.Log;
+import android.webkit.ConsoleMessage;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * A class that stores consoles messages, database callbacks, alert messages, etc.
+ */
+public class AdditionalTextOutput {
+ private static final String LOG_TAG = "AdditionalTextOutput";
+
+ /**
+ * Ordering of enums is important as it determines ordering of the toString method!
+ * StringBuilders will be printed in the order the corresponding types appear here.
+ */
+ private enum OutputType {
+ JS_DIALOG,
+ EXCEEDED_DB_QUOTA_MESSAGE,
+ CONSOLE_MESSAGE;
+ }
+
+ StringBuilder[] mOutputs = new StringBuilder[OutputType.values().length];
+
+ private StringBuilder getStringBuilderForType(OutputType outputType) {
+ int index = outputType.ordinal();
+ if (mOutputs[index] == null) {
+ mOutputs[index] = new StringBuilder();
+ }
+ return mOutputs[index];
+ }
+
+ public void appendExceededDbQuotaMessage(String urlString, String databaseIdentifier) {
+ StringBuilder output = getStringBuilderForType(OutputType.EXCEEDED_DB_QUOTA_MESSAGE);
+
+ String protocol = "";
+ String host = "";
+ int port = 0;
+
+ try {
+ URL url = new URL(urlString);
+ protocol = url.getProtocol();
+ host = url.getHost();
+ if (url.getPort() > -1) {
+ port = url.getPort();
+ }
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "urlString=" + urlString + " databaseIdentifier=" + databaseIdentifier,
+ e);
+ }
+
+ output.append("UI DELEGATE DATABASE CALLBACK: ");
+ output.append("exceededDatabaseQuotaForSecurityOrigin:{");
+ output.append(protocol + ", " + host + ", " + port + "} ");
+ output.append("database:" + databaseIdentifier + "\n");
+ }
+
+ public void appendConsoleMessage(ConsoleMessage consoleMessage) {
+ StringBuilder output = getStringBuilderForType(OutputType.CONSOLE_MESSAGE);
+
+ output.append("CONSOLE MESSAGE: line " + consoleMessage.lineNumber());
+ output.append(": " + consoleMessage.message() + "\n");
+ }
+
+ public void appendJsAlert(String message) {
+ StringBuilder output = getStringBuilderForType(OutputType.JS_DIALOG);
+
+ output.append("ALERT: ");
+ output.append(message);
+ output.append('\n');
+ }
+
+ public void appendJsConfirm(String message) {
+ StringBuilder output = getStringBuilderForType(OutputType.JS_DIALOG);
+
+ output.append("CONFIRM: ");
+ output.append(message);
+ output.append('\n');
+ }
+
+ public void appendJsPrompt(String message, String defaultValue) {
+ StringBuilder output = getStringBuilderForType(OutputType.JS_DIALOG);
+
+ output.append("PROMPT: ");
+ output.append(message);
+ output.append(", default text: ");
+ output.append(defaultValue);
+ output.append('\n');
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < mOutputs.length; i++) {
+ if (mOutputs[i] != null) {
+ result.append(mOutputs[i].toString());
+ }
+ }
+ return result.toString();
+ }
+} \ No newline at end of file
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..4831168
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/CrashedDummyResult.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ *
+ * TODO: All the methods regarding expected results need implementing.
+ */
+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.NO_ACTUAL_RESULT;
+ }
+
+ @Override
+ public boolean didCrash() {
+ return true;
+ }
+
+ @Override
+ public boolean didTimeOut() {
+ return false;
+ }
+
+ @Override
+ public void setDidTimeOut() {
+ /** This method is not applicable for this type of result */
+ assert false;
+ }
+
+ @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 */
+ }
+
+ @Override
+ public String getExpectedImageResultPath() {
+ /** TODO */
+ return null;
+ }
+
+ @Override
+ public String getExpectedTextResultPath() {
+ /** TODO */
+ return null;
+ }
+
+ @Override
+ public void setExpectedImageResultPath(String relativePath) {
+ /** TODO */
+ }
+
+ @Override
+ public void setExpectedTextResultPath(String relativePath) {
+ /** TODO */
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSender.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSender.java
new file mode 100644
index 0000000..5b7cbc4
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSender.java
@@ -0,0 +1,110 @@
+/*
+ * 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.webkit.WebView;
+
+/**
+ * A class that acts as a JS interface for webview to mock various touch events,
+ * mouse actions and key presses.
+ *
+ * The methods here just call corresponding methods on EventSenderImpl
+ * that contains the logic of how to execute the methods.
+ */
+public class EventSender {
+ EventSenderImpl mEventSenderImpl = new EventSenderImpl();
+
+ public void reset(WebView webView) {
+ mEventSenderImpl.reset(webView);
+ }
+
+ public void enableDOMUIEventLogging(int domNode) {
+ mEventSenderImpl.enableDOMUIEventLogging(domNode);
+ }
+
+ public void fireKeyboardEventsToElement(int domNode) {
+ mEventSenderImpl.fireKeyboardEventsToElement(domNode);
+ }
+
+ public void keyDown(String character, String[] withModifiers) {
+ mEventSenderImpl.keyDown(character, withModifiers);
+ }
+
+ public void keyDown(String character) {
+ keyDown(character, null);
+ }
+
+ public void leapForward(int milliseconds) {
+ mEventSenderImpl.leapForward(milliseconds);
+ }
+
+ public void mouseClick() {
+ mEventSenderImpl.mouseClick();
+ }
+
+ public void mouseDown() {
+ mEventSenderImpl.mouseDown();
+ }
+
+ public void mouseMoveTo(int x, int y) {
+ mEventSenderImpl.mouseMoveTo(x, y);
+ }
+
+ public void mouseUp() {
+ mEventSenderImpl.mouseUp();
+ }
+
+ public void touchStart() {
+ mEventSenderImpl.touchStart();
+ }
+
+ public void addTouchPoint(int x, int y) {
+ mEventSenderImpl.addTouchPoint(x, y);
+ }
+
+ public void updateTouchPoint(int id, int x, int y) {
+ mEventSenderImpl.updateTouchPoint(id, x, y);
+ }
+
+ public void setTouchModifier(String modifier, boolean enabled) {
+ mEventSenderImpl.setTouchModifier(modifier, enabled);
+ }
+
+ public void touchMove() {
+ mEventSenderImpl.touchMove();
+ }
+
+ public void releaseTouchPoint(int id) {
+ mEventSenderImpl.releaseTouchPoint(id);
+ }
+
+ public void touchEnd() {
+ mEventSenderImpl.touchEnd();
+ }
+
+ public void touchCancel() {
+ mEventSenderImpl.touchCancel();
+ }
+
+ public void clearTouchPoints() {
+ mEventSenderImpl.clearTouchPoints();
+ }
+
+ public void cancelTouchPoint(int id) {
+ mEventSenderImpl.cancelTouchPoint(id);
+ }
+} \ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSenderImpl.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSenderImpl.java
new file mode 100644
index 0000000..68bcf11
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSenderImpl.java
@@ -0,0 +1,580 @@
+/*
+ * 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.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.webkit.WebView;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * An implementation of EventSender
+ */
+public class EventSenderImpl {
+ private static final String LOG_TAG = "EventSenderImpl";
+
+ private static final int MSG_ENABLE_DOM_UI_EVENT_LOGGING = 0;
+ private static final int MSG_FIRE_KEYBOARD_EVENTS_TO_ELEMENT = 1;
+ private static final int MSG_LEAP_FORWARD = 2;
+
+ private static final int MSG_KEY_DOWN = 3;
+
+ private static final int MSG_MOUSE_DOWN = 4;
+ private static final int MSG_MOUSE_UP = 5;
+ private static final int MSG_MOUSE_CLICK = 6;
+ private static final int MSG_MOUSE_MOVE_TO = 7;
+
+ private static final int MSG_ADD_TOUCH_POINT = 8;
+ private static final int MSG_TOUCH_START = 9;
+ private static final int MSG_UPDATE_TOUCH_POINT = 10;
+ private static final int MSG_TOUCH_MOVE = 11;
+ private static final int MSG_CLEAR_TOUCH_POINTS = 12;
+ private static final int MSG_TOUCH_CANCEL = 13;
+ private static final int MSG_RELEASE_TOUCH_POINT = 14;
+ private static final int MSG_TOUCH_END = 15;
+ private static final int MSG_SET_TOUCH_MODIFIER = 16;
+ private static final int MSG_CANCEL_TOUCH_POINT = 17;
+
+ public static class TouchPoint {
+ WebView mWebView;
+ private int mId;
+ private int mX;
+ private int mY;
+ private long mDownTime;
+ private boolean mReleased = false;
+ private boolean mMoved = false;
+ private boolean mCancelled = false;
+
+ public TouchPoint(WebView webView, int id, int x, int y) {
+ mWebView = webView;
+ mId = id;
+ mX = scaleX(x);
+ mY = scaleY(y);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public int getX() {
+ return mX;
+ }
+
+ public int getY() {
+ return mY;
+ }
+
+ public boolean hasMoved() {
+ return mMoved;
+ }
+
+ public void move(int newX, int newY) {
+ mX = scaleX(newX);
+ mY = scaleY(newY);
+ mMoved = true;
+ }
+
+ public void resetHasMoved() {
+ mMoved = false;
+ }
+
+ public long getDownTime() {
+ return mDownTime;
+ }
+
+ public void setDownTime(long downTime) {
+ mDownTime = downTime;
+ }
+
+ public boolean isReleased() {
+ return mReleased;
+ }
+
+ public void release() {
+ mReleased = true;
+ }
+
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+
+ public void cancel() {
+ mCancelled = true;
+ }
+
+ private int scaleX(int x) {
+ return (int)(x * mWebView.getScale()) - mWebView.getScrollX();
+ }
+
+ private int scaleY(int y) {
+ return (int)(y * mWebView.getScale()) - mWebView.getScrollY();
+ }
+ }
+
+ private List<TouchPoint> mTouchPoints;
+ private int mTouchMetaState;
+ private int mMouseX;
+ private int mMouseY;
+
+ private WebView mWebView;
+
+ private Handler mEventSenderHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle bundle;
+ MotionEvent event;
+ long ts;
+
+ switch (msg.what) {
+ case MSG_ENABLE_DOM_UI_EVENT_LOGGING:
+ /** TODO: implement */
+ break;
+
+ case MSG_FIRE_KEYBOARD_EVENTS_TO_ELEMENT:
+ /** TODO: implement */
+ break;
+
+ case MSG_LEAP_FORWARD:
+ /** TODO: implement */
+ break;
+
+ case MSG_KEY_DOWN:
+ bundle = (Bundle)msg.obj;
+ String character = bundle.getString("character");
+ String[] withModifiers = bundle.getStringArray("withModifiers");
+
+ if (withModifiers != null && withModifiers.length > 0) {
+ for (int i = 0; i < withModifiers.length; i++) {
+ executeKeyEvent(KeyEvent.ACTION_DOWN,
+ modifierToKeyCode(withModifiers[i]));
+ }
+ }
+ executeKeyEvent(KeyEvent.ACTION_DOWN,
+ charToKeyCode(character.toLowerCase().toCharArray()[0]));
+ break;
+
+ /** MOUSE */
+
+ case MSG_MOUSE_DOWN:
+ ts = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(ts, ts, MotionEvent.ACTION_DOWN, mMouseX, mMouseY, 0);
+ mWebView.onTouchEvent(event);
+ break;
+
+ case MSG_MOUSE_UP:
+ ts = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(ts, ts, MotionEvent.ACTION_UP, mMouseX, mMouseY, 0);
+ mWebView.onTouchEvent(event);
+ break;
+
+ case MSG_MOUSE_CLICK:
+ mouseDown();
+ mouseUp();
+ break;
+
+ case MSG_MOUSE_MOVE_TO:
+ mMouseX = msg.arg1;
+ mMouseY = msg.arg2;
+ break;
+
+ /** TOUCH */
+
+ case MSG_ADD_TOUCH_POINT:
+ int numPoints = getTouchPoints().size();
+ int id;
+ if (numPoints == 0) {
+ id = 0;
+ } else {
+ id = getTouchPoints().get(numPoints - 1).getId() + 1;
+ }
+ getTouchPoints().add(new TouchPoint(mWebView, id,
+ msg.arg1, msg.arg2));
+ break;
+
+ case MSG_TOUCH_START:
+ if (getTouchPoints().isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < getTouchPoints().size(); ++i) {
+ getTouchPoints().get(i).setDownTime(SystemClock.uptimeMillis());
+ }
+ executeTouchEvent(MotionEvent.ACTION_DOWN);
+ break;
+
+ case MSG_UPDATE_TOUCH_POINT:
+ bundle = (Bundle)msg.obj;
+
+ int index = bundle.getInt("id");
+ if (index >= getTouchPoints().size()) {
+ Log.w(LOG_TAG + "::MSG_UPDATE_TOUCH_POINT", "TouchPoint out of bounds: "
+ + index);
+ break;
+ }
+
+ getTouchPoints().get(index).move(bundle.getInt("x"), bundle.getInt("y"));
+ break;
+
+ case MSG_TOUCH_MOVE:
+ /**
+ * FIXME: At the moment we don't support multi-touch. Hence, we only examine
+ * the first touch point. In future this method will need rewriting.
+ */
+ if (getTouchPoints().isEmpty()) {
+ return;
+ }
+ executeTouchEvent(MotionEvent.ACTION_MOVE);
+ for (int i = 0; i < getTouchPoints().size(); ++i) {
+ getTouchPoints().get(i).resetHasMoved();
+ }
+ break;
+
+ case MSG_CANCEL_TOUCH_POINT:
+ if (msg.arg1 >= getTouchPoints().size()) {
+ Log.w(LOG_TAG + "::MSG_RELEASE_TOUCH_POINT", "TouchPoint out of bounds: "
+ + msg.arg1);
+ break;
+ }
+
+ getTouchPoints().get(msg.arg1).cancel();
+ break;
+
+ case MSG_TOUCH_CANCEL:
+ /**
+ * FIXME: At the moment we don't support multi-touch. Hence, we only examine
+ * the first touch point. In future this method will need rewriting.
+ */
+ if (getTouchPoints().isEmpty()) {
+ return;
+ }
+ executeTouchEvent(MotionEvent.ACTION_CANCEL);
+ break;
+
+ case MSG_RELEASE_TOUCH_POINT:
+ if (msg.arg1 >= getTouchPoints().size()) {
+ Log.w(LOG_TAG + "::MSG_RELEASE_TOUCH_POINT", "TouchPoint out of bounds: "
+ + msg.arg1);
+ break;
+ }
+
+ getTouchPoints().get(msg.arg1).release();
+ break;
+
+ case MSG_TOUCH_END:
+ /**
+ * FIXME: At the moment we don't support multi-touch. Hence, we only examine
+ * the first touch point. In future this method will need rewriting.
+ */
+ if (getTouchPoints().isEmpty()) {
+ return;
+ }
+ executeTouchEvent(MotionEvent.ACTION_UP);
+ // remove released points.
+ for (int i = getTouchPoints().size() - 1; i >= 0; --i) {
+ if (getTouchPoints().get(i).isReleased()) {
+ getTouchPoints().remove(i);
+ }
+ }
+ break;
+
+ case MSG_SET_TOUCH_MODIFIER:
+ bundle = (Bundle)msg.obj;
+ String modifier = bundle.getString("modifier");
+ boolean enabled = bundle.getBoolean("enabled");
+
+ int mask = 0;
+ if ("alt".equals(modifier.toLowerCase())) {
+ mask = KeyEvent.META_ALT_ON;
+ } else if ("shift".equals(modifier.toLowerCase())) {
+ mask = KeyEvent.META_SHIFT_ON;
+ } else if ("ctrl".equals(modifier.toLowerCase())) {
+ mask = KeyEvent.META_SYM_ON;
+ }
+
+ if (enabled) {
+ mTouchMetaState |= mask;
+ } else {
+ mTouchMetaState &= ~mask;
+ }
+
+ break;
+
+ case MSG_CLEAR_TOUCH_POINTS:
+ getTouchPoints().clear();
+ break;
+
+ default:
+ break;
+ }
+ }
+ };
+
+ public void reset(WebView webView) {
+ mWebView = webView;
+ mTouchPoints = null;
+ mTouchMetaState = 0;
+ mMouseX = 0;
+ mMouseY = 0;
+ }
+
+ public void enableDOMUIEventLogging(int domNode) {
+ Message msg = mEventSenderHandler.obtainMessage(MSG_ENABLE_DOM_UI_EVENT_LOGGING);
+ msg.arg1 = domNode;
+ msg.sendToTarget();
+ }
+
+ public void fireKeyboardEventsToElement(int domNode) {
+ Message msg = mEventSenderHandler.obtainMessage(MSG_FIRE_KEYBOARD_EVENTS_TO_ELEMENT);
+ msg.arg1 = domNode;
+ msg.sendToTarget();
+ }
+
+ public void leapForward(int milliseconds) {
+ Message msg = mEventSenderHandler.obtainMessage(MSG_LEAP_FORWARD);
+ msg.arg1 = milliseconds;
+ msg.sendToTarget();
+ }
+
+ public void keyDown(String character, String[] withModifiers) {
+ Bundle bundle = new Bundle();
+ bundle.putString("character", character);
+ bundle.putStringArray("withModifiers", withModifiers);
+ mEventSenderHandler.obtainMessage(MSG_KEY_DOWN, bundle).sendToTarget();
+ }
+
+ /** MOUSE */
+
+ public void mouseDown() {
+ mEventSenderHandler.sendEmptyMessage(MSG_MOUSE_DOWN);
+ }
+
+ public void mouseUp() {
+ mEventSenderHandler.sendEmptyMessage(MSG_MOUSE_UP);
+ }
+
+ public void mouseClick() {
+ mEventSenderHandler.sendEmptyMessage(MSG_MOUSE_CLICK);
+ }
+
+ public void mouseMoveTo(int x, int y) {
+ mEventSenderHandler.obtainMessage(MSG_MOUSE_MOVE_TO, x, y).sendToTarget();
+ }
+
+ /** TOUCH */
+
+ public void addTouchPoint(int x, int y) {
+ mEventSenderHandler.obtainMessage(MSG_ADD_TOUCH_POINT, x, y).sendToTarget();
+ }
+
+ public void touchStart() {
+ mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_START);
+ }
+
+ public void updateTouchPoint(int id, int x, int y) {
+ Bundle bundle = new Bundle();
+ bundle.putInt("id", id);
+ bundle.putInt("x", x);
+ bundle.putInt("y", y);
+ mEventSenderHandler.obtainMessage(MSG_UPDATE_TOUCH_POINT, bundle).sendToTarget();
+ }
+
+ public void touchMove() {
+ mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_MOVE);
+ }
+
+ public void cancelTouchPoint(int id) {
+ Message msg = mEventSenderHandler.obtainMessage(MSG_CANCEL_TOUCH_POINT);
+ msg.arg1 = id;
+ msg.sendToTarget();
+ }
+
+ public void touchCancel() {
+ mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_CANCEL);
+ }
+
+ public void releaseTouchPoint(int id) {
+ Message msg = mEventSenderHandler.obtainMessage(MSG_RELEASE_TOUCH_POINT);
+ msg.arg1 = id;
+ msg.sendToTarget();
+ }
+
+ public void touchEnd() {
+ mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_END);
+ }
+
+ public void setTouchModifier(String modifier, boolean enabled) {
+ Bundle bundle = new Bundle();
+ bundle.putString("modifier", modifier);
+ bundle.putBoolean("enabled", enabled);
+ mEventSenderHandler.obtainMessage(MSG_SET_TOUCH_MODIFIER, bundle).sendToTarget();
+ }
+
+ public void clearTouchPoints() {
+ mEventSenderHandler.sendEmptyMessage(MSG_CLEAR_TOUCH_POINTS);
+ }
+
+ private List<TouchPoint> getTouchPoints() {
+ if (mTouchPoints == null) {
+ mTouchPoints = new LinkedList<TouchPoint>();
+ }
+
+ return mTouchPoints;
+ }
+
+ private void executeTouchEvent(int action) {
+ int numPoints = getTouchPoints().size();
+ int[] pointerIds = new int[numPoints];
+ MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[numPoints];
+
+ for (int i = 0; i < numPoints; ++i) {
+ boolean isNeeded = false;
+ switch(action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_UP:
+ isNeeded = true;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ isNeeded = getTouchPoints().get(i).hasMoved();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ isNeeded = getTouchPoints().get(i).isCancelled();
+ break;
+ default:
+ Log.w(LOG_TAG + "::executeTouchEvent(),", "action not supported:" + action);
+ break;
+ }
+
+ numPoints = 0;
+ if (isNeeded) {
+ pointerIds[numPoints] = getTouchPoints().get(i).getId();
+ pointerCoords[numPoints] = new MotionEvent.PointerCoords();
+ pointerCoords[numPoints].x = getTouchPoints().get(i).getX();
+ pointerCoords[numPoints].y = getTouchPoints().get(i).getY();
+ ++numPoints;
+ }
+ }
+
+ if (numPoints == 0) {
+ return;
+ }
+
+ MotionEvent event = MotionEvent.obtain(mTouchPoints.get(0).getDownTime(),
+ SystemClock.uptimeMillis(), action,
+ numPoints, pointerIds, pointerCoords,
+ mTouchMetaState, 1.0f, 1.0f, 0, 0, 0, 0);
+
+ mWebView.onTouchEvent(event);
+ }
+
+ private void executeKeyEvent(int action, int keyCode) {
+ KeyEvent event = new KeyEvent(action, keyCode);
+ mWebView.onKeyDown(event.getKeyCode(), event);
+ }
+
+ /**
+ * Assumes lowercase chars, case needs to be handled by calling function.
+ */
+ private static int charToKeyCode(char c) {
+ // handle numbers
+ if (c >= '0' && c <= '9') {
+ int offset = c - '0';
+ return KeyEvent.KEYCODE_0 + offset;
+ }
+
+ // handle characters
+ if (c >= 'a' && c <= 'z') {
+ int offset = c - 'a';
+ return KeyEvent.KEYCODE_A + offset;
+ }
+
+ // handle all others
+ switch (c) {
+ case '*':
+ return KeyEvent.KEYCODE_STAR;
+
+ case '#':
+ return KeyEvent.KEYCODE_POUND;
+
+ case ',':
+ return KeyEvent.KEYCODE_COMMA;
+
+ case '.':
+ return KeyEvent.KEYCODE_PERIOD;
+
+ case '\t':
+ return KeyEvent.KEYCODE_TAB;
+
+ case ' ':
+ return KeyEvent.KEYCODE_SPACE;
+
+ case '\n':
+ return KeyEvent.KEYCODE_ENTER;
+
+ case '\b':
+ case 0x7F:
+ return KeyEvent.KEYCODE_DEL;
+
+ case '~':
+ return KeyEvent.KEYCODE_GRAVE;
+
+ case '-':
+ return KeyEvent.KEYCODE_MINUS;
+
+ case '=':
+ return KeyEvent.KEYCODE_EQUALS;
+
+ case '(':
+ return KeyEvent.KEYCODE_LEFT_BRACKET;
+
+ case ')':
+ return KeyEvent.KEYCODE_RIGHT_BRACKET;
+
+ case '\\':
+ return KeyEvent.KEYCODE_BACKSLASH;
+
+ case ';':
+ return KeyEvent.KEYCODE_SEMICOLON;
+
+ case '\'':
+ return KeyEvent.KEYCODE_APOSTROPHE;
+
+ case '/':
+ return KeyEvent.KEYCODE_SLASH;
+
+ default:
+ return c;
+ }
+ }
+
+ private static int modifierToKeyCode(String modifier) {
+ if (modifier.equals("ctrlKey")) {
+ return KeyEvent.KEYCODE_ALT_LEFT;
+ } else if (modifier.equals("shiftKey")) {
+ return KeyEvent.KEYCODE_SHIFT_LEFT;
+ } else if (modifier.equals("altKey")) {
+ return KeyEvent.KEYCODE_SYM;
+ }
+
+ return KeyEvent.KEYCODE_UNKNOWN;
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/FileFilter.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/FileFilter.java
new file mode 100644
index 0000000..9bbf64a
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/FileFilter.java
@@ -0,0 +1,303 @@
+/*
+ * 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.util.Log;
+
+import com.android.dumprendertree2.forwarder.ForwarderManager;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A utility to filter out some files/directories from the views and tests that run.
+ */
+public class FileFilter {
+ private static final String LOG_TAG = "FileFilter";
+
+ private static final String TEST_EXPECTATIONS_TXT_PATH =
+ "platform/android/test_expectations.txt";
+
+ private static final String HTTP_TESTS_PATH = "http/tests/";
+ private static final String SSL_PATH = "ssl/";
+
+ private static final String TOKEN_CRASH = "CRASH";
+ private static final String TOKEN_FAIL = "FAIL";
+ private static final String TOKEN_SLOW = "SLOW";
+
+ private final Set<String> mCrashList = new HashSet<String>();
+ private final Set<String> mFailList = new HashSet<String>();
+ private final Set<String> mSlowList = new HashSet<String>();
+
+ public FileFilter() {
+ loadTestExpectations();
+ }
+
+ private static final String trimTrailingSlashIfPresent(String path) {
+ File file = new File(path);
+ return file.getPath();
+ }
+
+ public void loadTestExpectations() {
+ URL url = null;
+ try {
+ url = new URL(ForwarderManager.getHostSchemePort(false) + "LayoutTests/" +
+ TEST_EXPECTATIONS_TXT_PATH);
+ } catch (MalformedURLException e) {
+ assert false;
+ }
+
+ try {
+ InputStream inputStream = null;
+ BufferedReader bufferedReader = null;
+ try {
+ bufferedReader = new BufferedReader(new StringReader(new String(
+ FsUtils.readDataFromUrl(url))));
+
+ String line;
+ String entry;
+ String[] parts;
+ String path;
+ Set<String> tokens;
+ while (true) {
+ line = bufferedReader.readLine();
+ if (line == null) {
+ break;
+ }
+
+ /** Remove the comment and trim */
+ entry = line.split("//", 2)[0].trim();
+
+ /** Omit empty lines, advance to next line */
+ if (entry.isEmpty()) {
+ continue;
+ }
+
+ /** Split on whitespace into path part and the rest */
+ parts = entry.split("\\s", 2);
+
+ /** At this point parts.length >= 1 */
+ if (parts.length == 1) {
+ Log.w(LOG_TAG + "::reloadConfiguration",
+ "There are no options specified for the test!");
+ continue;
+ }
+
+ path = trimTrailingSlashIfPresent(parts[0]);
+
+ /** Split on whitespace */
+ tokens = new HashSet<String>(Arrays.asList(parts[1].split("\\s", 0)));
+
+ /** Chose the right collections to add to */
+ if (tokens.contains(TOKEN_CRASH)) {
+ mCrashList.add(path);
+
+ /** If test is on skip list we ignore any further options */
+ continue;
+ }
+
+ if (tokens.contains(TOKEN_FAIL)) {
+ mFailList.add(path);
+ }
+ if (tokens.contains(TOKEN_SLOW)) {
+ mSlowList.add(path);
+ }
+ }
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ if (bufferedReader != null) {
+ bufferedReader.close();
+ }
+ }
+ } catch (FileNotFoundException e) {
+ Log.w(LOG_TAG, "reloadConfiguration(): File not found: " + e.getMessage());
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "url=" + url, e);
+ }
+ }
+
+ /**
+ * Checks if test is expected to crash.
+ *
+ * <p>
+ * Path given should relative within LayoutTests folder, e.g. fast/dom/foo.html
+ *
+ * @param testPath
+ * - a relative path within LayoutTests folder
+ * @return if the test is supposed to be skipped
+ */
+ public boolean isCrash(String testPath) {
+ for (String prefix : getPrefixes(testPath)) {
+ if (mCrashList.contains(prefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if test result is supposed to be "failed".
+ *
+ * <p>
+ * Path given should relative within LayoutTests folder, e.g. fast/dom/foo.html
+ *
+ * @param testPath
+ * - a relative path within LayoutTests folder
+ * @return if the test result is supposed to be "failed"
+ */
+ public boolean isFail(String testPath) {
+ for (String prefix : getPrefixes(testPath)) {
+ if (mFailList.contains(prefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if test is slow and should have timeout increased.
+ *
+ * <p>
+ * Path given should relative within LayoutTests folder, e.g. fast/dom/foo.html
+ *
+ * @param testPath
+ * - a relative path within LayoutTests folder
+ * @return if the test is slow and should have timeout increased.
+ */
+ public boolean isSlow(String testPath) {
+ for (String prefix : getPrefixes(testPath)) {
+ if (mSlowList.contains(prefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the list of all path prefixes of the given path.
+ *
+ * <p>
+ * e.g. this/is/a/path returns the list: this this/is this/is/a this/is/a/path
+ *
+ * @param path
+ * @return the list of all path prefixes of the given path.
+ */
+ private static List<String> getPrefixes(String path) {
+ File file = new File(path);
+ List<String> prefixes = new ArrayList<String>(8);
+
+ do {
+ prefixes.add(file.getPath());
+ file = file.getParentFile();
+ } while (file != null);
+
+ return prefixes;
+ }
+
+ /**
+ * Checks if the directory may contain tests or contains just helper files.
+ *
+ * @param dirName
+ * @return
+ * if the directory may contain tests
+ */
+ public static boolean isTestDir(String dirName) {
+ return (!dirName.equals("script-tests")
+ && !dirName.equals("resources") && !dirName.startsWith("."));
+ }
+
+ /**
+ * Checks if the file is a test.
+ * Currently we run .html and .xhtml tests.
+ *
+ * @param testName
+ * @return
+ * if the file is a test
+ */
+ public static boolean isTestFile(String testName) {
+ return testName.endsWith(".html") || testName.endsWith(".xhtml");
+ }
+
+ /**
+ * Return a URL of the test on the server.
+ *
+ * @param relativePath
+ * @return a URL of the test on the server
+ */
+ public static URL getUrl(String relativePath) {
+ String urlBase = ForwarderManager.getHostSchemePort(false);
+
+ /**
+ * URL is formed differently for HTTP vs non-HTTP tests, because HTTP tests
+ * expect different document root. See run_apache2.py and .conf file for details
+ */
+ if (relativePath.startsWith(HTTP_TESTS_PATH)) {
+ relativePath = relativePath.substring(HTTP_TESTS_PATH.length());
+ if (relativePath.startsWith(SSL_PATH)) {
+ urlBase = ForwarderManager.getHostSchemePort(true);
+ }
+ } else {
+ relativePath = "LayoutTests/" + relativePath;
+ }
+
+ try {
+ return new URL(urlBase + relativePath);
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "Malformed URL!", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * If the path contains extension (e.g .foo at the end of the file) then it changes
+ * this (.foo) into newEnding (so it has to contain the dot if we want to preserve it).
+ *
+ * <p>If the path doesn't contain an extension, it adds the ending to the path.
+ *
+ * @param relativePath
+ * @param newEnding
+ * @return
+ * a new path, containing the newExtension
+ */
+ public static String setPathEnding(String relativePath, String newEnding) {
+ int dotPos = relativePath.lastIndexOf('.');
+ if (dotPos == -1) {
+ return relativePath + newEnding;
+ }
+
+ return relativePath.substring(0, dotPos) + newEnding;
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/FsUtils.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/FsUtils.java
new file mode 100644
index 0000000..4f9a737
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/FsUtils.java
@@ -0,0 +1,250 @@
+/*
+ * 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.util.Log;
+
+import com.android.dumprendertree2.forwarder.ForwarderManager;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ *
+ */
+public class FsUtils {
+ public static final String LOG_TAG = "FsUtils";
+
+ private static final String SCRIPT_URL = ForwarderManager.getHostSchemePort(false) +
+ "WebKitTools/DumpRenderTree/android/get_layout_tests_dir_contents.php";
+
+ private static final int HTTP_TIMEOUT_MS = 5000;
+
+ private static HttpClient sHttpClient;
+
+ private static HttpClient getHttpClient() {
+ if (sHttpClient == null) {
+ HttpParams params = new BasicHttpParams();
+
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(),
+ ForwarderManager.HTTP_PORT));
+ schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(),
+ ForwarderManager.HTTPS_PORT));
+
+ ClientConnectionManager connectionManager = new ThreadSafeClientConnManager(params,
+ schemeRegistry);
+ sHttpClient = new DefaultHttpClient(connectionManager, params);
+ HttpConnectionParams.setSoTimeout(sHttpClient.getParams(), HTTP_TIMEOUT_MS);
+ HttpConnectionParams.setConnectionTimeout(sHttpClient.getParams(), HTTP_TIMEOUT_MS);
+ }
+ return sHttpClient;
+ }
+
+ public static void writeDataToStorage(File file, byte[] bytes, boolean append) {
+ Log.d(LOG_TAG, "writeDataToStorage(): " + file.getAbsolutePath());
+ try {
+ OutputStream outputStream = null;
+ try {
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ Log.d(LOG_TAG, "writeDataToStorage(): File created: " + file.getAbsolutePath());
+ outputStream = new FileOutputStream(file, append);
+ outputStream.write(bytes);
+ } finally {
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "file.getAbsolutePath=" + file.getAbsolutePath() + " append=" + append,
+ e);
+ }
+ }
+
+ public static byte[] readDataFromStorage(File file) {
+ if (!file.exists()) {
+ Log.d(LOG_TAG, "readDataFromStorage(): File does not exist: "
+ + file.getAbsolutePath());
+ return null;
+ }
+
+ byte[] bytes = null;
+ try {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+ bytes = new byte[(int)file.length()];
+ fis.read(bytes);
+ } finally {
+ if (fis != null) {
+ fis.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "file.getAbsolutePath=" + file.getAbsolutePath(), e);
+ }
+
+ return bytes;
+ }
+
+ public static byte[] readDataFromUrl(URL url) {
+ if (url == null) {
+ Log.w(LOG_TAG, "readDataFromUrl(): url is null!");
+ return null;
+ }
+
+ HttpGet httpRequest = new HttpGet(url.toString());
+ ResponseHandler<byte[]> handler = new ResponseHandler<byte[]>() {
+ @Override
+ public byte[] handleResponse(HttpResponse response) throws IOException {
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ return null;
+ }
+ HttpEntity entity = response.getEntity();
+ return (entity == null ? null : EntityUtils.toByteArray(entity));
+ }
+ };
+
+ byte[] bytes = null;
+ try {
+ /**
+ * TODO: Not exactly sure why some requests hang indefinitely, but adding this
+ * timeout (in static getter for http client) in loop helps.
+ */
+ boolean timedOut;
+ do {
+ timedOut = false;
+ try {
+ bytes = getHttpClient().execute(httpRequest, handler);
+ } catch (SocketTimeoutException e) {
+ timedOut = true;
+ Log.w(LOG_TAG, "Expected SocketTimeoutException: " + url, e);
+ }
+ } while (timedOut);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "url=" + url, e);
+ }
+
+ return bytes;
+ }
+
+ public static List<String> getLayoutTestsDirContents(String dirRelativePath, boolean recurse,
+ boolean mode) {
+ String modeString = (mode ? "folders" : "files");
+
+ URL url = null;
+ try {
+ url = new URL(SCRIPT_URL +
+ "?path=" + dirRelativePath +
+ "&recurse=" + recurse +
+ "&mode=" + modeString);
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "path=" + dirRelativePath + " recurse=" + recurse + " mode=" +
+ modeString, e);
+ return new LinkedList<String>();
+ }
+
+ HttpGet httpRequest = new HttpGet(url.toString());
+ ResponseHandler<LinkedList<String>> handler = new ResponseHandler<LinkedList<String>>() {
+ @Override
+ public LinkedList<String> handleResponse(HttpResponse response)
+ throws IOException {
+ LinkedList<String> lines = new LinkedList<String>();
+
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ return lines;
+ }
+ HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ return lines;
+ }
+
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(entity.getContent()));
+ String line;
+ try {
+ while ((line = reader.readLine()) != null) {
+ lines.add(line);
+ }
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+
+ return lines;
+ }
+ };
+
+ try {
+ return getHttpClient().execute(httpRequest, handler);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "getLayoutTestsDirContents(): HTTP GET failed for URL " + url);
+ return null;
+ }
+ }
+
+ public static void closeInputStream(InputStream inputStream) {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Couldn't close stream!", e);
+ }
+ }
+
+ public static void closeOutputStream(OutputStream outputStream) {
+ try {
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Couldn't close stream!", e);
+ }
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestController.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestController.java
new file mode 100644
index 0000000..e608e2d
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestController.java
@@ -0,0 +1,116 @@
+/*
+ * 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.net.Uri;
+import android.util.Log;
+import android.webkit.MockGeolocation;
+import android.webkit.WebStorage;
+
+import java.io.File;
+
+/**
+ * A class that is registered as JS interface for webview in LayoutTestExecutor
+ */
+public class LayoutTestController {
+ private static final String LOG_TAG = "LayoutTestController";
+
+ LayoutTestsExecutor mLayoutTestsExecutor;
+
+ public LayoutTestController(LayoutTestsExecutor layoutTestsExecutor) {
+ mLayoutTestsExecutor = layoutTestsExecutor;
+ }
+
+ public void clearAllDatabases() {
+ Log.i(LOG_TAG, "clearAllDatabases() called");
+ WebStorage.getInstance().deleteAllData();
+ }
+
+ public void dumpAsText() {
+ dumpAsText(false);
+ }
+
+ public void dumpAsText(boolean enablePixelTest) {
+ mLayoutTestsExecutor.dumpAsText(enablePixelTest);
+ }
+
+ public void dumpChildFramesAsText() {
+ mLayoutTestsExecutor.dumpChildFramesAsText();
+ }
+
+ public void dumpDatabaseCallbacks() {
+ mLayoutTestsExecutor.dumpDatabaseCallbacks();
+ }
+
+ public void notifyDone() {
+ mLayoutTestsExecutor.notifyDone();
+ }
+
+ public void overridePreference(String key, boolean value) {
+ mLayoutTestsExecutor.overridePreference(key, value);
+ }
+
+ public void setAppCacheMaximumSize(long size) {
+ Log.i(LOG_TAG, "setAppCacheMaximumSize() called with: " + size);
+ WebStorage.getInstance().setAppCacheMaximumSize(size);
+ }
+
+ public void setCanOpenWindows() {
+ mLayoutTestsExecutor.setCanOpenWindows();
+ }
+
+ public void setDatabaseQuota(long quota) {
+ /** TODO: Reset this before every test! */
+ Log.i(LOG_TAG, "setDatabaseQuota() called with: " + quota);
+ WebStorage.getInstance().setQuotaForOrigin(Uri.fromFile(new File("")).toString(),
+ quota);
+ }
+
+ public void setGeolocationPermission(boolean allow) {
+ mLayoutTestsExecutor.setGeolocationPermission(allow);
+ }
+
+ public void setMockDeviceOrientation(boolean canProvideAlpha, double alpha,
+ boolean canProvideBeta, double beta, boolean canProvideGamma, double gamma) {
+ // Configuration is in WebKit, so stay on WebCore thread, but go via LayoutTestsExecutor
+ // as we need access to the Webview.
+ Log.i(LOG_TAG, "setMockDeviceOrientation(" + canProvideAlpha +
+ ", " + alpha + ", " + canProvideBeta + ", " + beta + ", " + canProvideGamma +
+ ", " + gamma + ")");
+ mLayoutTestsExecutor.setMockDeviceOrientation(
+ canProvideAlpha, alpha, canProvideBeta, beta, canProvideGamma, gamma);
+ }
+
+ public void setMockGeolocationError(int code, String message) {
+ Log.i(LOG_TAG, "setMockGeolocationError(): " + "code=" + code + " message=" + message);
+ MockGeolocation.getInstance().setError(code, message);
+ }
+
+ public void setMockGeolocationPosition(double latitude, double longitude, double accuracy) {
+ Log.i(LOG_TAG, "setMockGeolocationPosition(): " + "latitude=" + latitude +
+ " longitude=" + longitude + " accuracy=" + accuracy);
+ MockGeolocation.getInstance().setPosition(latitude, longitude, accuracy);
+ }
+
+ public void setXSSAuditorEnabled(boolean flag) {
+ mLayoutTestsExecutor.setXSSAuditorEnabled(flag);
+ }
+
+ public void waitUntilDone() {
+ mLayoutTestsExecutor.waitUntilDone();
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java
new file mode 100644
index 0000000..97d7cca
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java
@@ -0,0 +1,729 @@
+/*
+ * 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.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.PowerManager.WakeLock;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Window;
+import android.webkit.ConsoleMessage;
+import android.webkit.GeolocationPermissions;
+import android.webkit.HttpAuthHandler;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebStorage;
+import android.webkit.WebStorage.QuotaUpdater;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import java.io.File;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This activity executes the test. It contains WebView and logic of LayoutTestController
+ * functions. It runs in a separate process and sends the results of running the test
+ * to ManagerService. The reason why is to handle crashing (test that crashes brings down
+ * whole process with it).
+ */
+public class LayoutTestsExecutor extends Activity {
+
+ private enum CurrentState {
+ IDLE,
+ RENDERING_PAGE,
+ WAITING_FOR_ASYNCHRONOUS_TEST,
+ OBTAINING_RESULT;
+
+ public boolean isRunningState() {
+ return this == CurrentState.RENDERING_PAGE ||
+ this == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST;
+ }
+ }
+
+ private static final String LOG_TAG = "LayoutTestsExecutor";
+
+ public static final String EXTRA_TESTS_LIST = "TestsList";
+ public static final String EXTRA_TEST_INDEX = "TestIndex";
+
+ private static final int MSG_ACTUAL_RESULT_OBTAINED = 0;
+ private static final int MSG_TEST_TIMED_OUT = 1;
+
+ 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;
+
+ /**
+ * This is a number of currently running test. It is 0-based and doesn't reset after
+ * the crash. Initial index is passed to LayoutTestsExecuter in the intent that starts
+ * it.
+ */
+ private int mCurrentTestIndex;
+
+ /** The total number of tests to run, doesn't reset after crash */
+ private int mTotalTestCount;
+
+ private WebView mCurrentWebView;
+ private String mCurrentTestRelativePath;
+ private String mCurrentTestUri;
+ private CurrentState mCurrentState = CurrentState.IDLE;
+
+ private boolean mCurrentTestTimedOut;
+ private AbstractResult mCurrentResult;
+ private AdditionalTextOutput mCurrentAdditionalTextOutput;
+
+ private LayoutTestController mLayoutTestController = new LayoutTestController(this);
+ private boolean mCanOpenWindows;
+ private boolean mDumpDatabaseCallbacks;
+ private boolean mIsGeolocationPermissionSet;
+ private boolean mGeolocationPermission;
+ private Map<GeolocationPermissions.Callback, String> mPendingGeolocationPermissionCallbacks;
+
+ private EventSender mEventSender = new EventSender();
+
+ private WakeLock mScreenDimLock;
+
+ /** COMMUNICATION WITH ManagerService */
+
+ private Messenger mManagerServiceMessenger;
+
+ private ServiceConnection mServiceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mManagerServiceMessenger = new Messenger(service);
+ startTests();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ /** TODO */
+ }
+ };
+
+ private final Handler mResultHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ACTUAL_RESULT_OBTAINED:
+ onActualResultsObtained();
+ break;
+
+ case MSG_TEST_TIMED_OUT:
+ onTestTimedOut();
+ break;
+
+ default:
+ break;
+ }
+ }
+ };
+
+ /** WEBVIEW CONFIGURATION */
+
+ private WebViewClient mWebViewClient = new WebViewClient() {
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ /** Some tests fire up many page loads, we don't want to detect them */
+ if (!url.equals(mCurrentTestUri)) {
+ return;
+ }
+
+ if (mCurrentState == CurrentState.RENDERING_PAGE) {
+ onTestFinished();
+ }
+ }
+
+ @Override
+ public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler,
+ String host, String realm) {
+ if (handler.useHttpAuthUsernamePassword() && view != null) {
+ String[] credentials = view.getHttpAuthUsernamePassword(host, realm);
+ if (credentials != null && credentials.length == 2) {
+ handler.proceed(credentials[0], credentials[1]);
+ return;
+ }
+ }
+ handler.cancel();
+ }
+
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ // We ignore SSL errors. In particular, the certificate used by the LayoutTests server
+ // produces an error as it lacks a CN field.
+ handler.proceed();
+ }
+ };
+
+ private WebChromeClient mWebChromeClient = new WebChromeClient() {
+ @Override
+ public void onExceededDatabaseQuota(String url, String databaseIdentifier,
+ long currentQuota, long estimatedSize, long totalUsedQuota,
+ QuotaUpdater quotaUpdater) {
+ /** TODO: This should be recorded as part of the text result */
+ /** TODO: The quota should also probably be reset somehow for every test? */
+ if (mDumpDatabaseCallbacks) {
+ getCurrentAdditionalTextOutput().appendExceededDbQuotaMessage(url,
+ databaseIdentifier);
+ }
+ quotaUpdater.updateQuota(currentQuota + 5 * 1024 * 1024);
+ }
+
+ @Override
+ public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
+ getCurrentAdditionalTextOutput().appendJsAlert(message);
+ result.confirm();
+ return true;
+ }
+
+ @Override
+ public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
+ getCurrentAdditionalTextOutput().appendJsConfirm(message);
+ result.confirm();
+ return true;
+ }
+
+ @Override
+ public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
+ JsPromptResult result) {
+ getCurrentAdditionalTextOutput().appendJsPrompt(message, defaultValue);
+ result.confirm();
+ return true;
+ }
+
+ @Override
+ public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+ getCurrentAdditionalTextOutput().appendConsoleMessage(consoleMessage);
+ return true;
+ }
+
+ @Override
+ public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture,
+ Message resultMsg) {
+ WebView.WebViewTransport transport = (WebView.WebViewTransport)resultMsg.obj;
+ /** By default windows cannot be opened, so just send null back. */
+ WebView newWindowWebView = null;
+
+ if (mCanOpenWindows) {
+ /**
+ * We never display the new window, just create the view and allow it's content to
+ * execute and be recorded by the executor.
+ */
+ newWindowWebView = createWebViewWithJavascriptInterfaces();
+ setupWebView(newWindowWebView);
+ }
+
+ transport.setWebView(newWindowWebView);
+ resultMsg.sendToTarget();
+ return true;
+ }
+
+ @Override
+ public void onGeolocationPermissionsShowPrompt(String origin,
+ GeolocationPermissions.Callback callback) {
+ if (mIsGeolocationPermissionSet) {
+ callback.invoke(origin, mGeolocationPermission, false);
+ return;
+ }
+ if (mPendingGeolocationPermissionCallbacks == null) {
+ mPendingGeolocationPermissionCallbacks =
+ new HashMap<GeolocationPermissions.Callback, String>();
+ }
+ mPendingGeolocationPermissionCallbacks.put(callback, origin);
+ }
+ };
+
+ /** IMPLEMENTATION */
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ /**
+ * It detects the crash by catching all the uncaught exceptions. However, we
+ * still have to kill the process, because after catching the exception the
+ * activity remains in a strange state, where intents don't revive it.
+ * However, we send the message to the service to speed up the rebooting
+ * (we don't have to wait for time-out to kick in).
+ */
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable e) {
+ Log.w(LOG_TAG,
+ "onTestCrashed(): " + mCurrentTestRelativePath + " thread=" + thread, e);
+
+ try {
+ Message serviceMsg =
+ Message.obtain(null, ManagerService.MSG_CURRENT_TEST_CRASHED);
+
+ mManagerServiceMessenger.send(serviceMsg);
+ } catch (RemoteException e2) {
+ Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e2);
+ }
+
+ Process.killProcess(Process.myPid());
+ }
+ });
+
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+
+ Intent intent = getIntent();
+ mTestsList = intent.getStringArrayListExtra(EXTRA_TESTS_LIST);
+ mCurrentTestIndex = intent.getIntExtra(EXTRA_TEST_INDEX, -1);
+ mTotalTestCount = mCurrentTestIndex + mTestsList.size();
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mScreenDimLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK
+ | PowerManager.ON_AFTER_RELEASE, "WakeLock in LayoutTester");
+ mScreenDimLock.acquire();
+
+ bindService(new Intent(this, ManagerService.class), mServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ private void reset() {
+ WebView previousWebView = mCurrentWebView;
+
+ resetLayoutTestController();
+
+ mCurrentTestTimedOut = false;
+ mCurrentResult = null;
+ mCurrentAdditionalTextOutput = null;
+
+ mCurrentWebView = createWebViewWithJavascriptInterfaces();
+ // When we create the first WebView, we need to pause to wait for the WebView thread to spin
+ // and up and for it to register its message handlers.
+ if (previousWebView == null) {
+ try {
+ Thread.currentThread().sleep(1000);
+ } catch (Exception e) {}
+ }
+ setupWebView(mCurrentWebView);
+
+ mEventSender.reset(mCurrentWebView);
+
+ setContentView(mCurrentWebView);
+ if (previousWebView != null) {
+ Log.d(LOG_TAG + "::reset", "previousWebView != null");
+ previousWebView.destroy();
+ }
+ }
+
+ private static class WebViewWithJavascriptInterfaces extends WebView {
+ public WebViewWithJavascriptInterfaces(
+ Context context, Map<String, Object> javascriptInterfaces) {
+ super(context,
+ null, // attribute set
+ 0, // default style resource ID
+ javascriptInterfaces,
+ false); // is private browsing
+ }
+ }
+ private WebView createWebViewWithJavascriptInterfaces() {
+ Map<String, Object> javascriptInterfaces = new HashMap<String, Object>();
+ javascriptInterfaces.put("layoutTestController", mLayoutTestController);
+ javascriptInterfaces.put("eventSender", mEventSender);
+ return new WebViewWithJavascriptInterfaces(this, javascriptInterfaces);
+ }
+
+ private void setupWebView(WebView webView) {
+ webView.setWebViewClient(mWebViewClient);
+ webView.setWebChromeClient(mWebChromeClient);
+
+ /**
+ * Setting a touch interval of -1 effectively disables the optimisation in WebView
+ * that stops repeated touch events flooding WebCore. The Event Sender only sends a
+ * single event rather than a stream of events (like what would generally happen in
+ * a real use of touch events in a WebView) and so if the WebView drops the event,
+ * the test will fail as the test expects one callback for every touch it synthesizes.
+ */
+ webView.setTouchInterval(-1);
+
+ webView.clearCache(true);
+ webView.setDeferMultiTouch(true);
+
+ WebSettings webViewSettings = webView.getSettings();
+ webViewSettings.setAppCacheEnabled(true);
+ webViewSettings.setAppCachePath(getApplicationContext().getCacheDir().getPath());
+ // Use of larger values causes unexplained AppCache database corruption.
+ // TODO: Investigate what's really going on here.
+ webViewSettings.setAppCacheMaxSize(100 * 1024 * 1024);
+ webViewSettings.setJavaScriptEnabled(true);
+ webViewSettings.setJavaScriptCanOpenWindowsAutomatically(true);
+ webViewSettings.setSupportMultipleWindows(true);
+ webViewSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
+ webViewSettings.setDatabaseEnabled(true);
+ webViewSettings.setDatabasePath(getDir("databases", 0).getAbsolutePath());
+ webViewSettings.setDomStorageEnabled(true);
+ webViewSettings.setWorkersEnabled(false);
+ webViewSettings.setXSSAuditorEnabled(false);
+
+ // This is asynchronous, but it gets processed by WebCore before it starts loading pages.
+ mCurrentWebView.useMockDeviceOrientation();
+
+ // Must do this after setting the AppCache path.
+ WebStorage.getInstance().deleteAllData();
+ }
+
+ 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, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e);
+ }
+
+ runNextTest();
+ }
+
+ private void runNextTest() {
+ assert mCurrentState == CurrentState.IDLE : "mCurrentState = " + mCurrentState.name();
+
+ if (mTestsList.isEmpty()) {
+ onAllTestsFinished();
+ return;
+ }
+
+ mCurrentTestRelativePath = mTestsList.remove(0);
+
+ Log.i(LOG_TAG, "runNextTest(): Start: " + mCurrentTestRelativePath +
+ " (" + mCurrentTestIndex + ")");
+
+ mCurrentTestUri = FileFilter.getUrl(mCurrentTestRelativePath).toString();
+
+ reset();
+
+ /** Start time-out countdown and the test */
+ mCurrentState = CurrentState.RENDERING_PAGE;
+ mResultHandler.sendEmptyMessageDelayed(MSG_TEST_TIMED_OUT, DEFAULT_TIME_OUT_MS);
+ mCurrentWebView.loadUrl(mCurrentTestUri);
+ }
+
+ private void onTestTimedOut() {
+ assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name();
+
+ Log.w(LOG_TAG, "onTestTimedOut(): " + mCurrentTestRelativePath);
+ mCurrentTestTimedOut = true;
+
+ /**
+ * While it is theoretically possible that the test times out because
+ * of webview becoming unresponsive, it is very unlikely. Therefore it's
+ * assumed that obtaining results (that calls various webview methods)
+ * will not itself hang.
+ */
+ obtainActualResultsFromWebView();
+ }
+
+ private void onTestFinished() {
+ assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name();
+
+ Log.i(LOG_TAG, "onTestFinished(): " + mCurrentTestRelativePath);
+ mResultHandler.removeMessages(MSG_TEST_TIMED_OUT);
+ obtainActualResultsFromWebView();
+ }
+
+ private void obtainActualResultsFromWebView() {
+ /**
+ * If the result has not been set by the time the test finishes we create
+ * a default type of result.
+ */
+ if (mCurrentResult == null) {
+ /** TODO: Default type should be RenderTreeResult. We don't support it now. */
+ mCurrentResult = new TextResult(mCurrentTestRelativePath);
+ }
+
+ mCurrentState = CurrentState.OBTAINING_RESULT;
+
+ if (mCurrentTestTimedOut) {
+ mCurrentResult.setDidTimeOut();
+ }
+ mCurrentResult.obtainActualResults(mCurrentWebView,
+ mResultHandler.obtainMessage(MSG_ACTUAL_RESULT_OBTAINED));
+ }
+
+ private void onActualResultsObtained() {
+ assert mCurrentState == CurrentState.OBTAINING_RESULT
+ : "mCurrentState = " + mCurrentState.name();
+
+ Log.i(LOG_TAG, "onActualResultsObtained(): " + mCurrentTestRelativePath);
+ mCurrentState = CurrentState.IDLE;
+
+ reportResultToService();
+ mCurrentTestIndex++;
+ updateProgressBar();
+ runNextTest();
+ }
+
+ private void reportResultToService() {
+ if (mCurrentAdditionalTextOutput != null) {
+ mCurrentResult.setAdditionalTextOutputString(mCurrentAdditionalTextOutput.toString());
+ }
+
+ try {
+ Message serviceMsg =
+ Message.obtain(null, ManagerService.MSG_PROCESS_ACTUAL_RESULTS);
+
+ Bundle bundle = mCurrentResult.getBundle();
+ bundle.putInt("testIndex", mCurrentTestIndex);
+ if (!mTestsList.isEmpty()) {
+ bundle.putString("nextTest", mTestsList.get(0));
+ }
+
+ serviceMsg.setData(bundle);
+ mManagerServiceMessenger.send(serviceMsg);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e);
+ }
+ }
+
+ private void updateProgressBar() {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+ mCurrentTestIndex * Window.PROGRESS_END / mTotalTestCount);
+ setTitle(mCurrentTestIndex * 100 / mTotalTestCount + "% " +
+ "(" + mCurrentTestIndex + "/" + mTotalTestCount + ")");
+ }
+
+ private void onAllTestsFinished() {
+ mScreenDimLock.release();
+
+ try {
+ Message serviceMsg =
+ Message.obtain(null, ManagerService.MSG_ALL_TESTS_FINISHED);
+ mManagerServiceMessenger.send(serviceMsg);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e);
+ }
+
+ unbindService(mServiceConnection);
+ }
+
+ private AdditionalTextOutput getCurrentAdditionalTextOutput() {
+ if (mCurrentAdditionalTextOutput == null) {
+ mCurrentAdditionalTextOutput = new AdditionalTextOutput();
+ }
+ return mCurrentAdditionalTextOutput;
+ }
+
+ /** LAYOUT TEST CONTROLLER */
+
+ private static final int MSG_WAIT_UNTIL_DONE = 0;
+ private static final int MSG_NOTIFY_DONE = 1;
+ private static final int MSG_DUMP_AS_TEXT = 2;
+ private static final int MSG_DUMP_CHILD_FRAMES_AS_TEXT = 3;
+ private static final int MSG_SET_CAN_OPEN_WINDOWS = 4;
+ private static final int MSG_DUMP_DATABASE_CALLBACKS = 5;
+ private static final int MSG_SET_GEOLOCATION_PERMISSION = 6;
+ private static final int MSG_OVERRIDE_PREFERENCE = 7;
+ private static final int MSG_SET_XSS_AUDITOR_ENABLED = 8;
+
+ /** String constants for use with layoutTestController.overridePreference() */
+ private final String WEBKIT_OFFLINE_WEB_APPLICATION_CACHE_ENABLED =
+ "WebKitOfflineWebApplicationCacheEnabled";
+
+ Handler mLayoutTestControllerHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name();
+
+ switch (msg.what) {
+ case MSG_DUMP_AS_TEXT:
+ if (mCurrentResult == null) {
+ mCurrentResult = new TextResult(mCurrentTestRelativePath);
+ }
+ assert mCurrentResult instanceof TextResult
+ : "mCurrentResult instanceof" + mCurrentResult.getClass().getName();
+ break;
+
+ case MSG_DUMP_CHILD_FRAMES_AS_TEXT:
+ /** If dumpAsText was not called we assume that the result should be text */
+ if (mCurrentResult == null) {
+ mCurrentResult = new TextResult(mCurrentTestRelativePath);
+ }
+
+ assert mCurrentResult instanceof TextResult
+ : "mCurrentResult instanceof" + mCurrentResult.getClass().getName();
+
+ ((TextResult)mCurrentResult).setDumpChildFramesAsText(true);
+ break;
+
+ case MSG_DUMP_DATABASE_CALLBACKS:
+ mDumpDatabaseCallbacks = true;
+ break;
+
+ case MSG_NOTIFY_DONE:
+ if (mCurrentState == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST) {
+ onTestFinished();
+ }
+ break;
+
+ case MSG_OVERRIDE_PREFERENCE:
+ /**
+ * TODO: We should look up the correct WebView for the frame which
+ * called the layoutTestController method. Currently, we just use the
+ * WebView for the main frame. EventSender suffers from the same
+ * problem.
+ */
+ if (msg.getData().getString("key").equals(
+ WEBKIT_OFFLINE_WEB_APPLICATION_CACHE_ENABLED)) {
+ mCurrentWebView.getSettings().setAppCacheEnabled(msg.getData().getBoolean(
+ "value"));
+ } else {
+ Log.w(LOG_TAG, "MSG_OVERRIDE_PREFERENCE: unsupported preference!");
+ }
+ break;
+
+ case MSG_SET_CAN_OPEN_WINDOWS:
+ mCanOpenWindows = true;
+ break;
+
+ case MSG_SET_GEOLOCATION_PERMISSION:
+ mIsGeolocationPermissionSet = true;
+ mGeolocationPermission = msg.arg1 == 1;
+
+ if (mPendingGeolocationPermissionCallbacks != null) {
+ Iterator<GeolocationPermissions.Callback> iter =
+ mPendingGeolocationPermissionCallbacks.keySet().iterator();
+ while (iter.hasNext()) {
+ GeolocationPermissions.Callback callback = iter.next();
+ String origin = mPendingGeolocationPermissionCallbacks.get(callback);
+ callback.invoke(origin, mGeolocationPermission, false);
+ }
+ mPendingGeolocationPermissionCallbacks = null;
+ }
+ break;
+
+ case MSG_SET_XSS_AUDITOR_ENABLED:
+ mCurrentWebView.getSettings().setXSSAuditorEnabled(msg.arg1 == 1);
+ break;
+
+ case MSG_WAIT_UNTIL_DONE:
+ mCurrentState = CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST;
+ break;
+
+ default:
+ assert false : "msg.what=" + msg.what;
+ break;
+ }
+ }
+ };
+
+ private void resetLayoutTestController() {
+ mCanOpenWindows = false;
+ mDumpDatabaseCallbacks = false;
+ mIsGeolocationPermissionSet = false;
+ mPendingGeolocationPermissionCallbacks = null;
+ }
+
+ public void dumpAsText(boolean enablePixelTest) {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpAsText(" + enablePixelTest + ") called");
+ /** TODO: Implement */
+ if (enablePixelTest) {
+ Log.w(LOG_TAG, "enablePixelTest not implemented, switching to false");
+ }
+ mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_AS_TEXT);
+ }
+
+ public void dumpChildFramesAsText() {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpChildFramesAsText() called");
+ mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_CHILD_FRAMES_AS_TEXT);
+ }
+
+ public void dumpDatabaseCallbacks() {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpDatabaseCallbacks() called");
+ mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_DATABASE_CALLBACKS);
+ }
+
+ public void notifyDone() {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": notifyDone() called");
+ mLayoutTestControllerHandler.sendEmptyMessage(MSG_NOTIFY_DONE);
+ }
+
+ public void overridePreference(String key, boolean value) {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": overridePreference(" + key + ", " + value +
+ ") called");
+ Message msg = mLayoutTestControllerHandler.obtainMessage(MSG_OVERRIDE_PREFERENCE);
+ msg.getData().putString("key", key);
+ msg.getData().putBoolean("value", value);
+ msg.sendToTarget();
+ }
+
+ public void setCanOpenWindows() {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": setCanOpenWindows() called");
+ mLayoutTestControllerHandler.sendEmptyMessage(MSG_SET_CAN_OPEN_WINDOWS);
+ }
+
+ public void setGeolocationPermission(boolean allow) {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": setGeolocationPermission(" + allow +
+ ") called");
+ Message msg = mLayoutTestControllerHandler.obtainMessage(MSG_SET_GEOLOCATION_PERMISSION);
+ msg.arg1 = allow ? 1 : 0;
+ msg.sendToTarget();
+ }
+
+ public void setMockDeviceOrientation(boolean canProvideAlpha, double alpha,
+ boolean canProvideBeta, double beta, boolean canProvideGamma, double gamma) {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": setMockDeviceOrientation(" + canProvideAlpha +
+ ", " + alpha + ", " + canProvideBeta + ", " + beta + ", " + canProvideGamma +
+ ", " + gamma + ")");
+ mCurrentWebView.setMockDeviceOrientation(canProvideAlpha, alpha, canProvideBeta, beta,
+ canProvideGamma, gamma);
+ }
+
+ public void setXSSAuditorEnabled(boolean flag) {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": setXSSAuditorEnabled(" + flag + ") called");
+ Message msg = mLayoutTestControllerHandler.obtainMessage(MSG_SET_XSS_AUDITOR_ENABLED);
+ msg.arg1 = flag ? 1 : 0;
+ msg.sendToTarget();
+ }
+
+ public void waitUntilDone() {
+ Log.i(LOG_TAG, mCurrentTestRelativePath + ": waitUntilDone() called");
+ mLayoutTestControllerHandler.sendEmptyMessage(MSG_WAIT_UNTIL_DONE);
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java
new file mode 100644
index 0000000..f42dc86
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.java
@@ -0,0 +1,291 @@
+/*
+ * 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.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A service that handles managing the results of tests, informing of crashes, generating
+ * summaries, etc.
+ */
+public class ManagerService extends Service {
+
+ private static final String LOG_TAG = "ManagerService";
+
+ private static final int MSG_CRASH_TIMEOUT_EXPIRED = 0;
+ private static final int MSG_SUMMARIZER_DONE = 1;
+
+ private static final int CRASH_TIMEOUT_MS = 20 * 1000;
+
+ /** TODO: make it a setting */
+ static final String RESULTS_ROOT_DIR_PATH =
+ Environment.getExternalStorageDirectory() + File.separator + "layout-test-results";
+
+ /** TODO: Make it a setting */
+ private static final List<String> EXPECTED_RESULT_LOCATION_RELATIVE_DIR_PREFIXES =
+ new ArrayList<String>(3);
+ {
+ EXPECTED_RESULT_LOCATION_RELATIVE_DIR_PREFIXES.add("platform" + File.separator +
+ "android-v8" + File.separator);
+ EXPECTED_RESULT_LOCATION_RELATIVE_DIR_PREFIXES.add("platform" + File.separator +
+ "android" + File.separator);
+ EXPECTED_RESULT_LOCATION_RELATIVE_DIR_PREFIXES.add("");
+ }
+
+ /** TODO: Make these settings */
+ private static final String TEXT_RESULT_EXTENSION = "txt";
+ private static final String IMAGE_RESULT_EXTENSION = "png";
+
+ static final int MSG_PROCESS_ACTUAL_RESULTS = 0;
+ static final int MSG_ALL_TESTS_FINISHED = 1;
+ static final int MSG_FIRST_TEST = 2;
+ static final int MSG_CURRENT_TEST_CRASHED = 3;
+
+ /**
+ * 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:
+ mSummarizer.reset();
+ 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());
+ break;
+
+ case MSG_CURRENT_TEST_CRASHED:
+ mInternalMessagesHandler.removeMessages(MSG_CRASH_TIMEOUT_EXPIRED);
+ onTestCrashed();
+ break;
+
+ case MSG_ALL_TESTS_FINISHED:
+ /** We run it in a separate thread to avoid ANR */
+ new Thread() {
+ @Override
+ public void run() {
+ mSummarizer.setTestsRelativePath(mAllTestsRelativePath);
+ Message msg = Message.obtain(mInternalMessagesHandler,
+ MSG_SUMMARIZER_DONE);
+ mSummarizer.summarize(msg);
+ }
+ }.start();
+ }
+ }
+ };
+
+ private Messenger mMessenger = new Messenger(mIncomingHandler);
+
+ private Handler mInternalMessagesHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CRASH_TIMEOUT_EXPIRED:
+ onTestCrashed();
+ break;
+
+ case MSG_SUMMARIZER_DONE:
+ Intent intent = new Intent(ManagerService.this, TestsListActivity.class);
+ intent.setAction(Intent.ACTION_SHUTDOWN);
+ /** This flag is needed because we send the intent from the service */
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ break;
+ }
+ }
+ };
+
+ private FileFilter mFileFilter;
+ private Summarizer mSummarizer;
+
+ private String mCurrentlyRunningTest;
+ private int mCurrentlyRunningTestIndex;
+
+ /**
+ * These are implementation details of getExpectedResultPath() used to reduce the number
+ * of requests required to the host server.
+ */
+ private String mLastExpectedResultPathRequested;
+ private String mLastExpectedResultPathFetched;
+
+ private String mAllTestsRelativePath;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mFileFilter = new FileFilter();
+ mSummarizer = new Summarizer(mFileFilter, RESULTS_ROOT_DIR_PATH, getApplicationContext());
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mAllTestsRelativePath = intent.getStringExtra("path");
+ assert mAllTestsRelativePath != null;
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mMessenger.getBinder();
+ }
+
+ private void onActualResultsObtained(Bundle bundle) {
+ mInternalMessagesHandler.removeMessages(MSG_CRASH_TIMEOUT_EXPIRED);
+ ensureNextTestSetup(bundle.getString("nextTest"), bundle.getInt("testIndex") + 1);
+
+ AbstractResult results =
+ AbstractResult.TestType.valueOf(bundle.getString("type")).createResult(bundle);
+
+ Log.i(LOG_TAG, "onActualResultObtained: " + results.getRelativePath());
+ handleResults(results);
+ }
+
+ private void ensureNextTestSetup(String nextTest, int index) {
+ if (nextTest == null) {
+ Log.w(LOG_TAG, "ensureNextTestSetup(): nextTest=null");
+ return;
+ }
+
+ mCurrentlyRunningTest = nextTest;
+ mCurrentlyRunningTestIndex = index;
+ mInternalMessagesHandler.sendEmptyMessageDelayed(MSG_CRASH_TIMEOUT_EXPIRED, 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);
+ /** This flag is needed because we send the intent from the service */
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra("crashedTestIndex", mCurrentlyRunningTestIndex);
+ startActivity(intent);
+ }
+
+ private void handleResults(AbstractResult results) {
+ String relativePath = results.getRelativePath();
+ results.setExpectedTextResult(getExpectedTextResult(relativePath));
+ results.setExpectedTextResultPath(getExpectedTextResultPath(relativePath));
+ results.setExpectedImageResult(getExpectedImageResult(relativePath));
+ results.setExpectedImageResultPath(getExpectedImageResultPath(relativePath));
+
+ dumpActualTextResult(results);
+ dumpActualImageResult(results);
+
+ mSummarizer.appendTest(results);
+ }
+
+ private void dumpActualTextResult(AbstractResult result) {
+ String testPath = result.getRelativePath();
+ String actualTextResult = result.getActualTextResult();
+ if (actualTextResult == null) {
+ return;
+ }
+
+ String resultPath = FileFilter.setPathEnding(testPath, "-actual." + TEXT_RESULT_EXTENSION);
+ FsUtils.writeDataToStorage(new File(RESULTS_ROOT_DIR_PATH, resultPath),
+ actualTextResult.getBytes(), false);
+ }
+
+ private void dumpActualImageResult(AbstractResult result) {
+ String testPath = result.getRelativePath();
+ byte[] actualImageResult = result.getActualImageResult();
+ if (actualImageResult == null) {
+ return;
+ }
+
+ String resultPath = FileFilter.setPathEnding(testPath,
+ "-actual." + IMAGE_RESULT_EXTENSION);
+ FsUtils.writeDataToStorage(new File(RESULTS_ROOT_DIR_PATH, resultPath),
+ actualImageResult, false);
+ }
+
+ public String getExpectedTextResult(String relativePath) {
+ byte[] result = getExpectedResult(relativePath, TEXT_RESULT_EXTENSION);
+ if (result != null) {
+ return new String(result);
+ }
+ return null;
+ }
+
+ public byte[] getExpectedImageResult(String relativePath) {
+ return getExpectedResult(relativePath, IMAGE_RESULT_EXTENSION);
+ }
+
+ private byte[] getExpectedResult(String relativePath, String extension) {
+ String originalRelativePath =
+ FileFilter.setPathEnding(relativePath, "-expected." + extension);
+ mLastExpectedResultPathRequested = originalRelativePath;
+
+ byte[] bytes = null;
+ List<String> locations = EXPECTED_RESULT_LOCATION_RELATIVE_DIR_PREFIXES;
+
+ int size = EXPECTED_RESULT_LOCATION_RELATIVE_DIR_PREFIXES.size();
+ for (int i = 0; bytes == null && i < size; i++) {
+ relativePath = locations.get(i) + originalRelativePath;
+ bytes = FsUtils.readDataFromUrl(FileFilter.getUrl(relativePath));
+ }
+
+ mLastExpectedResultPathFetched = bytes == null ? null : relativePath;
+ return bytes;
+ }
+
+ private String getExpectedTextResultPath(String relativePath) {
+ return getExpectedResultPath(relativePath, TEXT_RESULT_EXTENSION);
+ }
+
+ private String getExpectedImageResultPath(String relativePath) {
+ return getExpectedResultPath(relativePath, IMAGE_RESULT_EXTENSION);
+ }
+
+ private String getExpectedResultPath(String relativePath, String extension) {
+ String originalRelativePath =
+ FileFilter.setPathEnding(relativePath, "-expected." + extension);
+ if (!originalRelativePath.equals(mLastExpectedResultPathRequested)) {
+ getExpectedResult(relativePath, extension);
+ }
+
+ return mLastExpectedResultPathFetched;
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/Summarizer.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/Summarizer.java
new file mode 100644
index 0000000..8d01a53
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/Summarizer.java
@@ -0,0 +1,573 @@
+/*
+ * 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.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Message;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import com.android.dumprendertree2.forwarder.ForwarderManager;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A class that collects information about tests that ran and can create HTML
+ * files with summaries and easy navigation.
+ */
+public class Summarizer {
+
+ private static final String LOG_TAG = "Summarizer";
+
+ private static final String CSS =
+ "<style type=\"text/css\">" +
+ "* {" +
+ " font-family: Verdana;" +
+ " border: 0;" +
+ " margin: 0;" +
+ " padding: 0;}" +
+ "body {" +
+ " margin: 10px;}" +
+ "h1 {" +
+ " font-size: 24px;" +
+ " margin: 4px 0 4px 0;}" +
+ "h2 {" +
+ " font-size:18px;" +
+ " text-transform: uppercase;" +
+ " margin: 20px 0 3px 0;}" +
+ "h3, h3 a {" +
+ " font-size: 14px;" +
+ " color: black;" +
+ " text-decoration: none;" +
+ " margin-top: 4px;" +
+ " margin-bottom: 2px;}" +
+ "h3 a span.path {" +
+ " text-decoration: underline;}" +
+ "h3 span.tri {" +
+ " text-decoration: none;" +
+ " float: left;" +
+ " width: 20px;}" +
+ "h3 span.sqr {" +
+ " text-decoration: none;" +
+ " float: left;" +
+ " width: 20px;}" +
+ "h3 span.sqr_pass {" +
+ " color: #8ee100;}" +
+ "h3 span.sqr_fail {" +
+ " color: #c30000;}" +
+ "span.source {" +
+ " display: block;" +
+ " font-size: 10px;" +
+ " color: #888;" +
+ " margin-left: 20px;" +
+ " margin-bottom: 1px;}" +
+ "span.source a {" +
+ " font-size: 10px;" +
+ " color: #888;}" +
+ "h3 img {" +
+ " width: 8px;" +
+ " margin-right: 4px;}" +
+ "div.diff {" +
+ " margin-bottom: 25px;}" +
+ "div.diff a {" +
+ " font-size: 12px;" +
+ " color: #888;}" +
+ "table.visual_diff {" +
+ " border-bottom: 0px solid;" +
+ " border-collapse: collapse;" +
+ " width: 100%;" +
+ " margin-bottom: 2px;}" +
+ "table.visual_diff tr.headers td {" +
+ " border-bottom: 1px solid;" +
+ " border-top: 0;" +
+ " padding-bottom: 3px;}" +
+ "table.visual_diff tr.results td {" +
+ " border-top: 1px dashed;" +
+ " border-right: 1px solid;" +
+ " font-size: 15px;" +
+ " vertical-align: top;}" +
+ "table.visual_diff tr.results td.line_count {" +
+ " background-color:#aaa;" +
+ " min-width:20px;" +
+ " text-align: right;" +
+ " border-right: 1px solid;" +
+ " border-left: 1px solid;" +
+ " padding: 2px 1px 2px 0px;}" +
+ "table.visual_diff tr.results td.line {" +
+ " padding: 2px 0px 2px 4px;" +
+ " border-right: 1px solid;" +
+ " width: 49.8%;}" +
+ "table.visual_diff tr.footers td {" +
+ " border-top: 1px solid;" +
+ " border-bottom: 0;}" +
+ "table.visual_diff tr td.space {" +
+ " border: 0;" +
+ " width: 0.4%}" +
+ "div.space {" +
+ " margin-top:4px;}" +
+ "span.eql {" +
+ " background-color: #f3f3f3;}" +
+ "span.del {" +
+ " background-color: #ff8888; }" +
+ "span.ins {" +
+ " background-color: #88ff88; }" +
+ "table.summary {" +
+ " border: 1px solid black;" +
+ " margin-top: 20px;}" +
+ "table.summary td {" +
+ " padding: 3px;}" +
+ "span.listItem {" +
+ " font-size: 11px;" +
+ " font-weight: normal;" +
+ " text-transform: uppercase;" +
+ " padding: 3px;" +
+ " -webkit-border-radius: 4px;}" +
+ "span." + AbstractResult.ResultCode.RESULTS_DIFFER.name() + "{" +
+ " background-color: #ccc;" +
+ " color: black;}" +
+ "span." + AbstractResult.ResultCode.NO_EXPECTED_RESULT.name() + "{" +
+ " background-color: #a700e4;" +
+ " color: #fff;}" +
+ "span.timed_out {" +
+ " background-color: #f3cb00;" +
+ " color: black;}" +
+ "span.crashed {" +
+ " background-color: #c30000;" +
+ " color: #fff;}" +
+ "span.noLtc {" +
+ " background-color: #944000;" +
+ " color: #fff;}" +
+ "span.noEventSender {" +
+ " background-color: #815600;" +
+ " color: #fff;}" +
+ "</style>";
+
+ private static final String SCRIPT =
+ "<script type=\"text/javascript\">" +
+ " function toggleDisplay(id) {" +
+ " element = document.getElementById(id);" +
+ " triangle = document.getElementById('tri.' + id);" +
+ " if (element.style.display == 'none') {" +
+ " element.style.display = 'inline';" +
+ " triangle.innerHTML = '&#x25bc; ';" +
+ " } else {" +
+ " element.style.display = 'none';" +
+ " triangle.innerHTML = '&#x25b6; ';" +
+ " }" +
+ " }" +
+ "</script>";
+
+ /** TODO: Make it a setting */
+ private static final String HTML_DETAILS_RELATIVE_PATH = "details.html";
+ private static final String TXT_SUMMARY_RELATIVE_PATH = "summary.txt";
+
+ private static final int RESULTS_PER_DUMP = 500;
+ private static final int RESULTS_PER_DB_ACCESS = 50;
+
+ private int mCrashedTestsCount = 0;
+ private List<AbstractResult> mUnexpectedFailures = new ArrayList<AbstractResult>();
+ private List<AbstractResult> mExpectedFailures = new ArrayList<AbstractResult>();
+ private List<AbstractResult> mExpectedPasses = new ArrayList<AbstractResult>();
+ private List<AbstractResult> mUnexpectedPasses = new ArrayList<AbstractResult>();
+
+ private Cursor mUnexpectedFailuresCursor;
+ private Cursor mExpectedFailuresCursor;
+ private Cursor mUnexpectedPassesCursor;
+ private Cursor mExpectedPassesCursor;
+
+ private FileFilter mFileFilter;
+ private String mResultsRootDirPath;
+ private String mTestsRelativePath;
+ private Date mDate;
+
+ private int mResultsSinceLastHtmlDump = 0;
+ private int mResultsSinceLastDbAccess = 0;
+
+ private SummarizerDBHelper mDbHelper;
+
+ public Summarizer(FileFilter fileFilter, String resultsRootDirPath, Context context) {
+ mFileFilter = fileFilter;
+ mResultsRootDirPath = resultsRootDirPath;
+
+ /**
+ * We don't run the database I/O in a separate thread to avoid consumer/producer problem
+ * and to simplify code.
+ */
+ mDbHelper = new SummarizerDBHelper(context);
+ mDbHelper.open();
+ }
+
+ public static URI getDetailsUri() {
+ return new File(ManagerService.RESULTS_ROOT_DIR_PATH + File.separator +
+ HTML_DETAILS_RELATIVE_PATH).toURI();
+ }
+
+ public void appendTest(AbstractResult result) {
+ String relativePath = result.getRelativePath();
+
+ if (result.didCrash()) {
+ mCrashedTestsCount++;
+ }
+
+ if (result.didPass()) {
+ result.clearResults();
+ if (mFileFilter.isFail(relativePath)) {
+ mUnexpectedPasses.add(result);
+ } else {
+ mExpectedPasses.add(result);
+ }
+ } else {
+ if (mFileFilter.isFail(relativePath)) {
+ mExpectedFailures.add(result);
+ } else {
+ mUnexpectedFailures.add(result);
+ }
+ }
+
+ if (++mResultsSinceLastDbAccess == RESULTS_PER_DB_ACCESS) {
+ persistLists();
+ clearLists();
+ }
+ }
+
+ private void clearLists() {
+ mUnexpectedFailures.clear();
+ mExpectedFailures.clear();
+ mUnexpectedPasses.clear();
+ mExpectedPasses.clear();
+ }
+
+ private void persistLists() {
+ persistListToTable(mUnexpectedFailures, SummarizerDBHelper.UNEXPECTED_FAILURES_TABLE);
+ persistListToTable(mExpectedFailures, SummarizerDBHelper.EXPECTED_FAILURES_TABLE);
+ persistListToTable(mUnexpectedPasses, SummarizerDBHelper.UNEXPECTED_PASSES_TABLE);
+ persistListToTable(mExpectedPasses, SummarizerDBHelper.EXPECTED_PASSES_TABLE);
+ mResultsSinceLastDbAccess = 0;
+ }
+
+ private void persistListToTable(List<AbstractResult> results, String table) {
+ for (AbstractResult abstractResult : results) {
+ mDbHelper.insertAbstractResult(abstractResult, table);
+ }
+ }
+
+ public void setTestsRelativePath(String testsRelativePath) {
+ mTestsRelativePath = testsRelativePath;
+ }
+
+ public void summarize(Message onFinishMessage) {
+ persistLists();
+ clearLists();
+
+ mUnexpectedFailuresCursor =
+ mDbHelper.getAbstractResults(SummarizerDBHelper.UNEXPECTED_FAILURES_TABLE);
+ mUnexpectedPassesCursor =
+ mDbHelper.getAbstractResults(SummarizerDBHelper.UNEXPECTED_PASSES_TABLE);
+ mExpectedFailuresCursor =
+ mDbHelper.getAbstractResults(SummarizerDBHelper.EXPECTED_FAILURES_TABLE);
+ mExpectedPassesCursor =
+ mDbHelper.getAbstractResults(SummarizerDBHelper.EXPECTED_PASSES_TABLE);
+
+ String webKitRevision = getWebKitRevision();
+ createHtmlDetails(webKitRevision);
+ createTxtSummary(webKitRevision);
+
+ clearLists();
+ mUnexpectedFailuresCursor.close();
+ mUnexpectedPassesCursor.close();
+ mExpectedFailuresCursor.close();
+ mExpectedPassesCursor.close();
+
+ onFinishMessage.sendToTarget();
+ }
+
+ public void reset() {
+ mCrashedTestsCount = 0;
+ clearLists();
+ mDbHelper.reset();
+ mDate = new Date();
+ }
+
+ private void dumpHtmlToFile(StringBuilder html, boolean append) {
+ FsUtils.writeDataToStorage(new File(mResultsRootDirPath, HTML_DETAILS_RELATIVE_PATH),
+ html.toString().getBytes(), append);
+ html.setLength(0);
+ mResultsSinceLastHtmlDump = 0;
+ }
+
+ private void createTxtSummary(String webKitRevision) {
+ StringBuilder txt = new StringBuilder();
+
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
+ txt.append("Path: " + mTestsRelativePath + "\n");
+ txt.append("Date: " + dateFormat.format(mDate) + "\n");
+ txt.append("Build fingerprint: " + Build.FINGERPRINT + "\n");
+ txt.append("WebKit version: " + getWebKitVersionFromUserAgentString() + "\n");
+ txt.append("WebKit revision: " + webKitRevision + "\n");
+
+ txt.append("TOTAL: " + getTotalTestCount() + "\n");
+ txt.append("CRASHED (among all tests): " + mCrashedTestsCount + "\n");
+ txt.append("UNEXPECTED FAILURES: " + mUnexpectedFailuresCursor.getCount() + "\n");
+ txt.append("UNEXPECTED PASSES: " + mUnexpectedPassesCursor.getCount() + "\n");
+ txt.append("EXPECTED FAILURES: " + mExpectedFailuresCursor.getCount() + "\n");
+ txt.append("EXPECTED PASSES: " + mExpectedPassesCursor.getCount() + "\n");
+
+ FsUtils.writeDataToStorage(new File(mResultsRootDirPath, TXT_SUMMARY_RELATIVE_PATH),
+ txt.toString().getBytes(), false);
+ }
+
+ private void createHtmlDetails(String webKitRevision) {
+ StringBuilder html = new StringBuilder();
+
+ html.append("<html><head>");
+ html.append(CSS);
+ html.append(SCRIPT);
+ html.append("</head><body>");
+
+ createTopSummaryTable(webKitRevision, html);
+ dumpHtmlToFile(html, false);
+
+ createResultsList(html, "Unexpected failures", mUnexpectedFailuresCursor);
+ createResultsList(html, "Unexpected passes", mUnexpectedPassesCursor);
+ createResultsList(html, "Expected failures", mExpectedFailuresCursor);
+ createResultsList(html, "Expected passes", mExpectedPassesCursor);
+
+ html.append("</body></html>");
+ dumpHtmlToFile(html, true);
+ }
+
+ private int getTotalTestCount() {
+ return mUnexpectedFailuresCursor.getCount() +
+ mUnexpectedPassesCursor.getCount() +
+ mExpectedPassesCursor.getCount() +
+ mExpectedFailuresCursor.getCount();
+ }
+
+ private String getWebKitVersionFromUserAgentString() {
+ Resources resources = new Resources(new AssetManager(), new DisplayMetrics(),
+ new Configuration());
+ String userAgent =
+ resources.getString(com.android.internal.R.string.web_user_agent);
+
+ Matcher matcher = Pattern.compile("AppleWebKit/([0-9]+?\\.[0-9])").matcher(userAgent);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return "unknown";
+ }
+
+ private String getWebKitRevision() {
+ URL url = null;
+ try {
+ url = new URL(ForwarderManager.getHostSchemePort(false) + "ThirdPartyProject.prop");
+ } catch (MalformedURLException e) {
+ assert false;
+ }
+
+ String thirdPartyProjectContents = new String(FsUtils.readDataFromUrl(url));
+ Matcher matcher = Pattern.compile("^version=([0-9]+)", Pattern.MULTILINE).matcher(
+ thirdPartyProjectContents);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return "unknown";
+ }
+
+ private void createTopSummaryTable(String webKitRevision, StringBuilder html) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
+ html.append("<h1>" + "Layout tests' results for: " +
+ (mTestsRelativePath.equals("") ? "all tests" : mTestsRelativePath) + "</h1>");
+ html.append("<h3>" + "Date: " + dateFormat.format(new Date()) + "</h3>");
+ html.append("<h3>" + "Build fingerprint: " + Build.FINGERPRINT + "</h3>");
+ html.append("<h3>" + "WebKit version: " + getWebKitVersionFromUserAgentString() + "</h3>");
+
+ html.append("<h3>" + "WebKit revision: ");
+ html.append("<a href=\"http://trac.webkit.org/browser/trunk?rev=" + webKitRevision +
+ "\" target=\"_blank\"><span class=\"path\">" + webKitRevision + "</span></a>");
+ html.append("</h3>");
+
+ html.append("<table class=\"summary\">");
+ createSummaryTableRow(html, "TOTAL", getTotalTestCount());
+ createSummaryTableRow(html, "CRASHED (among all tests)", mCrashedTestsCount);
+ createSummaryTableRow(html, "UNEXPECTED FAILURES", mUnexpectedFailuresCursor.getCount());
+ createSummaryTableRow(html, "UNEXPECTED PASSES", mUnexpectedPassesCursor.getCount());
+ createSummaryTableRow(html, "EXPECTED FAILURES", mExpectedFailuresCursor.getCount());
+ createSummaryTableRow(html, "EXPECTED PASSES", mExpectedPassesCursor.getCount());
+ html.append("</table>");
+ }
+
+ private void createSummaryTableRow(StringBuilder html, String caption, int size) {
+ html.append("<tr>");
+ html.append(" <td>" + caption + "</td>");
+ html.append(" <td>" + size + "</td>");
+ html.append("</tr>");
+ }
+
+ private void createResultsList(
+ StringBuilder html, String title, Cursor cursor) {
+ String relativePath;
+ String id = "";
+ AbstractResult.ResultCode resultCode;
+
+ html.append("<h2>" + title + " [" + cursor.getCount() + "]</h2>");
+
+ if (!cursor.moveToFirst()) {
+ return;
+ }
+
+ AbstractResult result;
+ do {
+ result = SummarizerDBHelper.getAbstractResult(cursor);
+
+ relativePath = result.getRelativePath();
+ resultCode = result.getResultCode();
+
+ html.append("<h3>");
+
+ /**
+ * Technically, two different paths could end up being the same, because
+ * ':' is a valid character in a path. However, it is probably not going
+ * to cause any problems in this case
+ */
+ id = relativePath.replace(File.separator, ":");
+
+ /** Write the test name */
+ if (resultCode == AbstractResult.ResultCode.RESULTS_DIFFER) {
+ html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');");
+ html.append("return false;\">");
+ html.append("<span class=\"tri\" id=\"tri." + id + "\">&#x25b6; </span>");
+ html.append("<span class=\"path\">" + relativePath + "</span>");
+ html.append("</a>");
+ } else {
+ html.append("<a href=\"" + getViewSourceUrl(result.getRelativePath()).toString() + "\"");
+ html.append(" target=\"_blank\">");
+ html.append("<span class=\"sqr sqr_" + (result.didPass() ? "pass" : "fail"));
+ html.append("\">&#x25a0; </span>");
+ html.append("<span class=\"path\">" + result.getRelativePath() + "</span>");
+ html.append("</a>");
+ }
+
+ if (!result.didPass()) {
+ appendTags(html, result);
+ }
+
+ html.append("</h3>");
+ appendExpectedResultsSources(result, html);
+
+ if (resultCode == AbstractResult.ResultCode.RESULTS_DIFFER) {
+ html.append("<div class=\"diff\" style=\"display: none;\" id=\"" + id + "\">");
+ html.append(result.getDiffAsHtml());
+ html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');");
+ html.append("return false;\">Hide</a>");
+ html.append(" | ");
+ html.append("<a href=\"" + getViewSourceUrl(relativePath).toString() + "\"");
+ html.append(" target=\"_blank\">Show source</a>");
+ html.append("</div>");
+ }
+
+ html.append("<div class=\"space\"></div>");
+
+ if (++mResultsSinceLastHtmlDump == RESULTS_PER_DUMP) {
+ dumpHtmlToFile(html, true);
+ }
+
+ cursor.moveToNext();
+ } while (!cursor.isAfterLast());
+ }
+
+ private void appendTags(StringBuilder html, AbstractResult result) {
+ /** Tag tests which crash, time out or where results don't match */
+ if (result.didCrash()) {
+ html.append(" <span class=\"listItem crashed\">Crashed</span>");
+ } else {
+ if (result.didTimeOut()) {
+ html.append(" <span class=\"listItem timed_out\">Timed out</span>");
+ }
+ AbstractResult.ResultCode resultCode = result.getResultCode();
+ if (resultCode != AbstractResult.ResultCode.RESULTS_MATCH) {
+ html.append(" <span class=\"listItem " + resultCode.name() + "\">");
+ html.append(resultCode.toString());
+ html.append("</span>");
+ }
+ }
+
+ /** Detect missing LTC function */
+ String additionalTextOutputString = result.getAdditionalTextOutputString();
+ if (additionalTextOutputString != null &&
+ additionalTextOutputString.contains("com.android.dumprendertree") &&
+ additionalTextOutputString.contains("has no method")) {
+ if (additionalTextOutputString.contains("LayoutTestController")) {
+ html.append(" <span class=\"listItem noLtc\">LTC function missing</span>");
+ }
+ if (additionalTextOutputString.contains("EventSender")) {
+ html.append(" <span class=\"listItem noEventSender\">");
+ html.append("ES function missing</span>");
+ }
+ }
+ }
+
+ private static final void appendExpectedResultsSources(AbstractResult result,
+ StringBuilder html) {
+ String textSource = result.getExpectedTextResultPath();
+ String imageSource = result.getExpectedImageResultPath();
+
+ if (textSource == null) {
+ // Show if a text result is missing. We may want to revisit this decision when we add
+ // support for image results.
+ html.append("<span class=\"source\">Expected textual result missing</span>");
+ } else {
+ html.append("<span class=\"source\">Expected textual result from: ");
+ html.append("<a href=\"" + ForwarderManager.getHostSchemePort(false) + "LayoutTests/" +
+ textSource + "\"");
+ html.append(" target=\"_blank\">");
+ html.append(textSource + "</a></span>");
+ }
+ if (imageSource != null) {
+ html.append("<span class=\"source\">Expected image result from: ");
+ html.append("<a href=\"" + ForwarderManager.getHostSchemePort(false) + "LayoutTests/" +
+ imageSource + "\"");
+ html.append(" target=\"_blank\">");
+ html.append(imageSource + "</a></span>");
+ }
+ }
+
+ private static final URL getViewSourceUrl(String relativePath) {
+ URL url = null;
+ try {
+ url = new URL("http", "localhost", ForwarderManager.HTTP_PORT,
+ "/WebKitTools/DumpRenderTree/android/view_source.php?src=" +
+ relativePath);
+ } catch (MalformedURLException e) {
+ assert false : "relativePath=" + relativePath;
+ }
+ return url;
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/SummarizerDBHelper.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/SummarizerDBHelper.java
new file mode 100644
index 0000000..23e13ec
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/SummarizerDBHelper.java
@@ -0,0 +1,129 @@
+/*
+ * 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.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A basic class that wraps database accesses inside itself and provides functionality to
+ * store and retrieve AbstractResults.
+ */
+public class SummarizerDBHelper {
+ private static final String KEY_ID = "id";
+ private static final String KEY_PATH = "path";
+ private static final String KEY_BYTES = "bytes";
+
+ private static final String DATABASE_NAME = "SummarizerDB";
+ private static final int DATABASE_VERSION = 1;
+
+ static final String EXPECTED_FAILURES_TABLE = "expectedFailures";
+ static final String UNEXPECTED_FAILURES_TABLE = "unexpectedFailures";
+ static final String EXPECTED_PASSES_TABLE = "expextedPasses";
+ static final String UNEXPECTED_PASSES_TABLE = "unexpextedPasses";
+ private static final Set<String> TABLES_NAMES = new HashSet<String>();
+ {
+ TABLES_NAMES.add(EXPECTED_FAILURES_TABLE);
+ TABLES_NAMES.add(EXPECTED_PASSES_TABLE);
+ TABLES_NAMES.add(UNEXPECTED_FAILURES_TABLE);
+ TABLES_NAMES.add(UNEXPECTED_PASSES_TABLE);
+ }
+
+ private static final void createTables(SQLiteDatabase db) {
+ String cmd;
+ for (String tableName : TABLES_NAMES) {
+ cmd = "create table " + tableName + " ("
+ + KEY_ID + " integer primary key autoincrement, "
+ + KEY_PATH + " text not null, "
+ + KEY_BYTES + " blob not null);";
+ db.execSQL(cmd);
+ }
+ }
+
+ private static final void dropTables(SQLiteDatabase db) {
+ for (String tableName : TABLES_NAMES) {
+ db.execSQL("DROP TABLE IF EXISTS " + tableName);
+ }
+ }
+
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ /** NOOP for now, because we will never upgrade the db */
+ }
+
+ public void reset(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+ }
+
+ private DatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+
+ private final Context mContext;
+
+ public SummarizerDBHelper(Context ctx) {
+ mContext = ctx;
+ mDbHelper = new DatabaseHelper(mContext);
+ }
+
+ public void reset() {
+ mDbHelper.reset(this.mDb);
+ }
+
+ public void open() throws SQLException {
+ mDb = mDbHelper.getWritableDatabase();
+ }
+
+ public void close() {
+ mDbHelper.close();
+ }
+
+ public void insertAbstractResult(AbstractResult result, String table) {
+ ContentValues cv = new ContentValues();
+ cv.put(KEY_PATH, result.getRelativePath());
+ cv.put(KEY_BYTES, result.getBytes());
+ mDb.insert(table, null, cv);
+ }
+
+ public Cursor getAbstractResults(String table) throws SQLException {
+ return mDb.query(false, table, new String[] {KEY_BYTES}, null, null, null, null,
+ KEY_PATH + " ASC", null);
+ }
+
+ public static AbstractResult getAbstractResult(Cursor cursor) {
+ return AbstractResult.create(cursor.getBlob(cursor.getColumnIndex(KEY_BYTES)));
+ }
+} \ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java
new file mode 100644
index 0000000..9db4d2b
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java
@@ -0,0 +1,202 @@
+/*
+ * 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.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Gravity;
+import android.view.Window;
+import android.webkit.WebView;
+import android.widget.Toast;
+
+import com.android.dumprendertree2.scriptsupport.OnEverythingFinishedCallback;
+
+import java.util.ArrayList;
+
+/**
+ * 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.
+ */
+public class TestsListActivity extends Activity {
+
+ private static final int MSG_TEST_LIST_PRELOADER_DONE = 0;
+
+ /** Constants for adding extras to an intent */
+ public static final String EXTRA_TEST_PATH = "TestPath";
+
+ private static ProgressDialog sProgressDialog;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_TEST_LIST_PRELOADER_DONE:
+ sProgressDialog.dismiss();
+ mTestsList = (ArrayList<String>)msg.obj;
+ mTotalTestCount = mTestsList.size();
+ restartExecutor(0);
+ break;
+ }
+ }
+ };
+
+ private ArrayList<String> mTestsList;
+ private int mTotalTestCount;
+
+ private OnEverythingFinishedCallback mOnEverythingFinishedCallback;
+ private boolean mEverythingFinished;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ /** Prepare the progress dialog */
+ sProgressDialog = new ProgressDialog(TestsListActivity.this);
+ sProgressDialog.setCancelable(false);
+ sProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ sProgressDialog.setTitle(R.string.dialog_progress_title);
+ sProgressDialog.setMessage(getText(R.string.dialog_progress_msg));
+
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+
+ Intent intent = getIntent();
+ if (!intent.getAction().equals(Intent.ACTION_RUN)) {
+ return;
+ }
+ String path = intent.getStringExtra(EXTRA_TEST_PATH);
+
+ sProgressDialog.show();
+ Message doneMsg = Message.obtain(mHandler, MSG_TEST_LIST_PRELOADER_DONE);
+
+ Intent serviceIntent = new Intent(this, ManagerService.class);
+ serviceIntent.putExtra("path", path);
+ startService(serviceIntent);
+
+ new TestsListPreloaderThread(path, doneMsg).start();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_REBOOT)) {
+ onCrashIntent(intent);
+ } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
+ onEverythingFinishedIntent(intent);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private void onCrashIntent(Intent intent) {
+ int nextTestToRun = intent.getIntExtra("crashedTestIndex", -1) + 1;
+ if (nextTestToRun > 0 && nextTestToRun <= mTotalTestCount) {
+ restartExecutor(nextTestToRun);
+ }
+ }
+
+ public void registerOnEverythingFinishedCallback(OnEverythingFinishedCallback callback) {
+ mOnEverythingFinishedCallback = callback;
+ if (mEverythingFinished) {
+ mOnEverythingFinishedCallback.onFinished();
+ }
+ }
+
+ private void onEverythingFinishedIntent(Intent intent) {
+ Toast toast = Toast.makeText(this,
+ "All tests finished.\nPress back key to return to the tests' list.",
+ Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, -40, 0);
+ toast.show();
+
+ /** Show the details to the user */
+ WebView webView = new WebView(this);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.getSettings().setBuiltInZoomControls(true);
+ webView.getSettings().setEnableSmoothTransition(true);
+ /** This enables double-tap to zoom */
+ webView.getSettings().setUseWideViewPort(true);
+
+ setContentView(webView);
+ webView.loadUrl(Summarizer.getDetailsUri().toString());
+
+ mEverythingFinished = true;
+ if (mOnEverythingFinishedCallback != null) {
+ mOnEverythingFinishedCallback.onFinished();
+ }
+ }
+
+ /**
+ * This, together with android:configChanges="orientation" in manifest file, prevents
+ * the activity from restarting on orientation change.
+ */
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ }
+
+ @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.
+ *
+ * @param startFrom
+ * test index in mTestsList to start the tests from (inclusive, 0-based)
+ */
+ private void restartExecutor(int startFrom) {
+ Intent intent = new Intent();
+ intent.setClass(this, LayoutTestsExecutor.class);
+ intent.setAction(Intent.ACTION_RUN);
+
+ 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
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListPreloaderThread.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListPreloaderThread.java
new file mode 100644
index 0000000..0e7d47a
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListPreloaderThread.java
@@ -0,0 +1,114 @@
+/*
+ * 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.Environment;
+import android.os.Message;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A Thread that is responsible for generating a lists of tests to run.
+ */
+public class TestsListPreloaderThread extends Thread {
+
+ private static final String LOG_TAG = "TestsListPreloaderThread";
+
+ /** A list containing relative paths of tests to run */
+ private ArrayList<String> mTestsList = new ArrayList<String>();
+
+ private FileFilter mFileFilter;
+
+ /**
+ * A relative path to the directory with the tests we want to run or particular test.
+ * Used up to and including preloadTests().
+ */
+ private String mRelativePath;
+
+ private Message mDoneMsg;
+
+ /**
+ * The given path must be relative to the root dir.
+ *
+ * @param path
+ * @param doneMsg
+ */
+ public TestsListPreloaderThread(String path, Message doneMsg) {
+ mFileFilter = new FileFilter();
+ mRelativePath = path;
+ mDoneMsg = doneMsg;
+ }
+
+ @Override
+ public void run() {
+ if (FileFilter.isTestFile(mRelativePath)) {
+ mTestsList.add(mRelativePath);
+ } else {
+ loadTestsFromUrl(mRelativePath);
+ }
+
+ mDoneMsg.obj = mTestsList;
+ mDoneMsg.sendToTarget();
+ }
+
+ /**
+ * Loads all the tests from the given directories and all the subdirectories
+ * into mTestsList.
+ *
+ * @param dirRelativePath
+ */
+ private void loadTestsFromUrl(String rootRelativePath) {
+ LinkedList<String> directoriesList = new LinkedList<String>();
+ directoriesList.add(rootRelativePath);
+
+ String relativePath;
+ String itemName;
+ while (!directoriesList.isEmpty()) {
+ relativePath = directoriesList.removeFirst();
+
+ List<String> dirRelativePaths = FsUtils.getLayoutTestsDirContents(relativePath, false, true);
+ if (dirRelativePaths != null) {
+ for (String dirRelativePath : dirRelativePaths) {
+ itemName = new File(dirRelativePath).getName();
+ if (FileFilter.isTestDir(itemName)) {
+ directoriesList.add(dirRelativePath);
+ }
+ }
+ }
+
+ List<String> testRelativePaths = FsUtils.getLayoutTestsDirContents(relativePath, false, false);
+ if (testRelativePaths != null) {
+ for (String testRelativePath : testRelativePaths) {
+ itemName = new File(testRelativePath).getName();
+ if (FileFilter.isTestFile(itemName)) {
+ /** We choose to skip all the tests that are expected to crash. */
+ if (!mFileFilter.isCrash(testRelativePath)) {
+ mTestsList.add(testRelativePath);
+ } else {
+ /**
+ * TODO: Summarizer is now in service - figure out how to send the info.
+ * Previously: mSummarizer.addSkippedTest(relativePath);
+ */
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/TextResult.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TextResult.java
new file mode 100644
index 0000000..3d2b98b
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TextResult.java
@@ -0,0 +1,256 @@
+/*
+ * 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.Handler;
+import android.os.Message;
+import android.webkit.WebView;
+
+import name.fraser.neil.plaintext.diff_match_patch;
+
+import java.util.LinkedList;
+
+/**
+ * A result object for which the expected output is text. It does not have an image
+ * expected result.
+ *
+ * <p>Created if layoutTestController.dumpAsText() was called.
+ */
+public class TextResult extends AbstractResult {
+
+ private static final int MSG_DOCUMENT_AS_TEXT = 0;
+
+ private String mExpectedResult;
+ private String mExpectedResultPath;
+ private String mActualResult;
+ private String mRelativePath;
+ private boolean mDidTimeOut;
+ private ResultCode mResultCode;
+ transient private Message mResultObtainedMsg;
+
+ private boolean mDumpChildFramesAsText;
+
+ transient private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_DOCUMENT_AS_TEXT) {
+ mActualResult = (String)msg.obj;
+ mResultObtainedMsg.sendToTarget();
+ }
+ }
+ };
+
+ public TextResult(String relativePath) {
+ mRelativePath = relativePath;
+ }
+
+ public void setDumpChildFramesAsText(boolean dumpChildFramesAsText) {
+ mDumpChildFramesAsText = dumpChildFramesAsText;
+ }
+
+ /**
+ * Used to recreate the Result when received by the service.
+ *
+ * @param bundle
+ * bundle with data used to recreate the result
+ */
+ public TextResult(Bundle bundle) {
+ mExpectedResult = bundle.getString("expectedTextualResult");
+ mExpectedResultPath = bundle.getString("expectedTextualResultPath");
+ mActualResult = bundle.getString("actualTextualResult");
+ setAdditionalTextOutputString(bundle.getString("additionalTextOutputString"));
+ mRelativePath = bundle.getString("relativePath");
+ mDidTimeOut = bundle.getBoolean("didTimeOut");
+ }
+
+ @Override
+ public void clearResults() {
+ super.clearResults();
+ mExpectedResult = null;
+ mActualResult = null;
+ }
+
+ @Override
+ public ResultCode getResultCode() {
+ if (mResultCode == null) {
+ mResultCode = resultsMatch() ? AbstractResult.ResultCode.RESULTS_MATCH
+ : AbstractResult.ResultCode.RESULTS_DIFFER;
+ }
+ return mResultCode;
+ }
+
+ private boolean resultsMatch() {
+ assert mExpectedResult != null;
+ assert mActualResult != null;
+ // Trim leading and trailing empty lines, as other WebKit platforms do.
+ String leadingEmptyLines = "^\\n+";
+ String trailingEmptyLines = "\\n+$";
+ String trimmedExpectedResult = mExpectedResult.replaceFirst(leadingEmptyLines, "")
+ .replaceFirst(trailingEmptyLines, "");
+ String trimmedActualResult = mActualResult.replaceFirst(leadingEmptyLines, "")
+ .replaceFirst(trailingEmptyLines, "");
+ return trimmedExpectedResult.equals(trimmedActualResult);
+ }
+
+ @Override
+ public boolean didCrash() {
+ return false;
+ }
+
+ @Override
+ public boolean didTimeOut() {
+ return mDidTimeOut;
+ }
+
+ @Override
+ public void setDidTimeOut() {
+ mDidTimeOut = true;
+ }
+
+ @Override
+ public byte[] getActualImageResult() {
+ return null;
+ }
+
+ @Override
+ public String getActualTextResult() {
+ String additionalTextResultString = getAdditionalTextOutputString();
+ if (additionalTextResultString != null) {
+ return additionalTextResultString + mActualResult;
+ }
+
+ return mActualResult;
+ }
+
+ @Override
+ public void setExpectedImageResult(byte[] expectedResult) {
+ /** This method is not applicable to this type of result */
+ }
+
+ @Override
+ public void setExpectedImageResultPath(String relativePath) {
+ /** This method is not applicable to this type of result */
+ }
+
+ @Override
+ public String getExpectedImageResultPath() {
+ /** This method is not applicable to this type of result */
+ return null;
+ }
+
+ @Override
+ public void setExpectedTextResultPath(String relativePath) {
+ mExpectedResultPath = relativePath;
+ }
+
+ @Override
+ public String getExpectedTextResultPath() {
+ return mExpectedResultPath;
+ }
+
+ @Override
+ public void setExpectedTextResult(String expectedResult) {
+ // For text results, we use an empty string for the expected result when none is
+ // present, as other WebKit platforms do.
+ mExpectedResult = expectedResult == null ? "" : expectedResult;
+ }
+
+ @Override
+ public String getDiffAsHtml() {
+ assert mExpectedResult != null;
+ assert mActualResult != null;
+
+ StringBuilder html = new StringBuilder();
+ html.append("<table class=\"visual_diff\">");
+ html.append(" <tr class=\"headers\">");
+ html.append(" <td colspan=\"2\">Expected result:</td>");
+ html.append(" <td class=\"space\"></td>");
+ html.append(" <td colspan=\"2\">Actual result:</td>");
+ html.append(" </tr>");
+
+ appendDiffHtml(html);
+
+ html.append(" <tr class=\"footers\">");
+ html.append(" <td colspan=\"2\"></td>");
+ html.append(" <td class=\"space\"></td>");
+ html.append(" <td colspan=\"2\"></td>");
+ html.append(" </tr>");
+ html.append("</table>");
+
+ return html.toString();
+ }
+
+ private void appendDiffHtml(StringBuilder html) {
+ LinkedList<diff_match_patch.Diff> diffs =
+ new diff_match_patch().diff_main(mExpectedResult, mActualResult);
+
+ diffs = VisualDiffUtils.splitDiffsOnNewline(diffs);
+
+ LinkedList<String> expectedLines = new LinkedList<String>();
+ LinkedList<Integer> expectedLineNums = new LinkedList<Integer>();
+ LinkedList<String> actualLines = new LinkedList<String>();
+ LinkedList<Integer> actualLineNums = new LinkedList<Integer>();
+
+ VisualDiffUtils.generateExpectedResultLines(diffs, expectedLineNums, expectedLines);
+ VisualDiffUtils.generateActualResultLines(diffs, actualLineNums, actualLines);
+ // TODO: We should use a map for each line number and lines pair.
+ assert expectedLines.size() == expectedLineNums.size();
+ assert actualLines.size() == actualLineNums.size();
+ assert expectedLines.size() == actualLines.size();
+
+ html.append(VisualDiffUtils.getHtml(expectedLineNums, expectedLines,
+ actualLineNums, actualLines));
+ }
+
+ @Override
+ public TestType getType() {
+ return TestType.TEXT;
+ }
+
+ @Override
+ public void obtainActualResults(WebView webview, Message resultObtainedMsg) {
+ mResultObtainedMsg = resultObtainedMsg;
+ Message msg = mHandler.obtainMessage(MSG_DOCUMENT_AS_TEXT);
+
+ /**
+ * arg1 - should dump top frame as text
+ * arg2 - should dump child frames as text
+ */
+ msg.arg1 = 1;
+ msg.arg2 = mDumpChildFramesAsText ? 1 : 0;
+ webview.documentAsText(msg);
+ }
+
+ @Override
+ public Bundle getBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString("expectedTextualResult", mExpectedResult);
+ bundle.putString("expectedTextualResultPath", mExpectedResultPath);
+ bundle.putString("actualTextualResult", getActualTextResult());
+ bundle.putString("additionalTextOutputString", getAdditionalTextOutputString());
+ bundle.putString("relativePath", mRelativePath);
+ bundle.putBoolean("didTimeOut", mDidTimeOut);
+ bundle.putString("type", getType().name());
+ return bundle;
+ }
+
+ @Override
+ public String getRelativePath() {
+ return mRelativePath;
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/VisualDiffUtils.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/VisualDiffUtils.java
new file mode 100644
index 0000000..d7f7313
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/VisualDiffUtils.java
@@ -0,0 +1,214 @@
+/*
+ * 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 name.fraser.neil.plaintext.diff_match_patch;
+
+import java.util.LinkedList;
+
+/**
+ * Helper methods fo TextResult.getDiffAsHtml()
+ */
+public class VisualDiffUtils {
+
+ private static final int DONT_PRINT_LINE_NUMBER = -1;
+
+ /**
+ * Preprocesses the list of diffs so that new line characters appear only at the end of
+ * diff.text
+ *
+ * @param diffs
+ * @return
+ * LinkedList of diffs where new line character appears only on the end of
+ * diff.text
+ */
+ public static LinkedList<diff_match_patch.Diff> splitDiffsOnNewline(
+ LinkedList<diff_match_patch.Diff> diffs) {
+ LinkedList<diff_match_patch.Diff> newDiffs = new LinkedList<diff_match_patch.Diff>();
+
+ String[] parts;
+ int lengthMinusOne;
+ for (diff_match_patch.Diff diff : diffs) {
+ parts = diff.text.split("\n", -1);
+ if (parts.length == 1) {
+ newDiffs.add(diff);
+ continue;
+ }
+
+ lengthMinusOne = parts.length - 1;
+ for (int i = 0; i < lengthMinusOne; i++) {
+ newDiffs.add(new diff_match_patch.Diff(diff.operation, parts[i] + "\n"));
+ }
+ if (!parts[lengthMinusOne].isEmpty()) {
+ newDiffs.add(new diff_match_patch.Diff(diff.operation, parts[lengthMinusOne]));
+ }
+ }
+
+ return newDiffs;
+ }
+
+ public static void generateExpectedResultLines(LinkedList<diff_match_patch.Diff> diffs,
+ LinkedList<Integer> lineNums, LinkedList<String> lines) {
+ String delSpan = "<span class=\"del\">";
+ String eqlSpan = "<span class=\"eql\">";
+
+ String line = "";
+ int i = 1;
+ diff_match_patch.Diff diff;
+ int size = diffs.size();
+ boolean isLastDiff;
+ for (int j = 0; j < size; j++) {
+ diff = diffs.get(j);
+ isLastDiff = j == size - 1;
+ switch (diff.operation) {
+ case DELETE:
+ line = processDiff(diff, lineNums, lines, line, i, delSpan, isLastDiff);
+ if (line.equals("")) {
+ i++;
+ }
+ break;
+
+ case INSERT:
+ // If the line is currently empty and this insertion is the entire line, the
+ // expected line is absent, so it has no line number.
+ if (diff.text.endsWith("\n") || isLastDiff) {
+ lineNums.add(line.equals("") ? DONT_PRINT_LINE_NUMBER : i++);
+ lines.add(line);
+ line = "";
+ }
+ break;
+
+ case EQUAL:
+ line = processDiff(diff, lineNums, lines, line, i, eqlSpan, isLastDiff);
+ if (line.equals("")) {
+ i++;
+ }
+ break;
+ }
+ }
+ }
+
+ public static void generateActualResultLines(LinkedList<diff_match_patch.Diff> diffs,
+ LinkedList<Integer> lineNums, LinkedList<String> lines) {
+ String insSpan = "<span class=\"ins\">";
+ String eqlSpan = "<span class=\"eql\">";
+
+ String line = "";
+ int i = 1;
+ diff_match_patch.Diff diff;
+ int size = diffs.size();
+ boolean isLastDiff;
+ for (int j = 0; j < size; j++) {
+ diff = diffs.get(j);
+ isLastDiff = j == size - 1;
+ switch (diff.operation) {
+ case INSERT:
+ line = processDiff(diff, lineNums, lines, line, i, insSpan, isLastDiff);
+ if (line.equals("")) {
+ i++;
+ }
+ break;
+
+ case DELETE:
+ // If the line is currently empty and deletion is the entire line, the
+ // actual line is absent, so it has no line number.
+ if (diff.text.endsWith("\n") || isLastDiff) {
+ lineNums.add(line.equals("") ? DONT_PRINT_LINE_NUMBER : i++);
+ lines.add(line);
+ line = "";
+ }
+ break;
+
+ case EQUAL:
+ line = processDiff(diff, lineNums, lines, line, i, eqlSpan, isLastDiff);
+ if (line.equals("")) {
+ i++;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Generate or append a line for a given diff and add it to given collections if necessary.
+ * It puts diffs in HTML spans.
+ *
+ * @param diff
+ * @param lineNums
+ * @param lines
+ * @param line
+ * @param i
+ * @param begSpan
+ * @param forceOutputLine Force the current line to be output
+ * @return
+ */
+ public static String processDiff(diff_match_patch.Diff diff, LinkedList<Integer> lineNums,
+ LinkedList<String> lines, String line, int i, String begSpan, boolean forceOutputLine) {
+ String endSpan = "</span>";
+ String br = "&nbsp;";
+
+ if (diff.text.endsWith("\n") || forceOutputLine) {
+ lineNums.add(i);
+ /** TODO: Think of better way to replace stuff */
+ line += begSpan + diff.text.replace(" ", "&nbsp;&nbsp;")
+ + endSpan + br;
+ lines.add(line);
+ line = "";
+ } else {
+ line += begSpan + diff.text.replace(" ", "&nbsp;&nbsp;") + endSpan;
+ }
+
+ return line;
+ }
+
+ public static String getHtml(LinkedList<Integer> lineNums1, LinkedList<String> lines1,
+ LinkedList<Integer> lineNums2, LinkedList<String> lines2) {
+ StringBuilder html = new StringBuilder();
+ int lineNum;
+ int size = lines1.size();
+ for (int i = 0; i < size; i++) {
+ html.append("<tr class=\"results\">");
+
+ html.append(" <td class=\"line_count\">");
+ lineNum = lineNums1.removeFirst();
+ if (lineNum > 0) {
+ html.append(lineNum);
+ }
+ html.append(" </td>");
+
+ html.append(" <td class=\"line\">");
+ html.append(lines1.removeFirst());
+ html.append(" </td>");
+
+ html.append(" <td class=\"space\"></td>");
+
+ html.append(" <td class=\"line_count\">");
+ lineNum = lineNums2.removeFirst();
+ if (lineNum > 0) {
+ html.append(lineNum);
+ }
+ html.append(" </td>");
+
+ html.append(" <td class=\"line\">");
+ html.append(lines2.removeFirst());
+ html.append(" </td>");
+
+ html.append("</tr>");
+ }
+ return html.toString();
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/AdbUtils.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/AdbUtils.java
new file mode 100644
index 0000000..224509d
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/AdbUtils.java
@@ -0,0 +1,75 @@
+/*
+ * 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.forwarder;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * The utility class that can setup a socket allowing the device to communicate with remote
+ * machines through the machine that the device is connected to via adb.
+ */
+public class AdbUtils {
+ private static final String LOG_TAG = "AdbUtils";
+
+ private static final String ADB_OK = "OKAY";
+ private static final int ADB_PORT = 5037;
+ private static final String ADB_HOST = "127.0.0.1";
+ private static final int ADB_RESPONSE_SIZE = 4;
+
+ /**
+ * Creates a new socket that can be configured to serve as a transparent proxy to a
+ * remote machine. This can be achieved by calling configureSocket()
+ *
+ * @return a socket that can be configured to link to remote machine
+ * @throws IOException
+ */
+ public static Socket createSocket() throws IOException{
+ return new Socket(ADB_HOST, ADB_PORT);
+ }
+
+ /**
+ * Configures the connection to serve as a transparent proxy to a remote machine.
+ * The given streams must belong to a socket created by createSocket().
+ *
+ * @param inputStream inputStream of the socket we want to configure
+ * @param outputStream outputStream of the socket we want to configure
+ * @param remoteAddress address of the remote machine (as you would type in a browser
+ * in a machine that the device is connected to via adb)
+ * @param remotePort port on which to connect
+ * @return if the configuration suceeded
+ * @throws IOException
+ */
+ public static boolean configureConnection(InputStream inputStream, OutputStream outputStream,
+ String remoteAddress, int remotePort) throws IOException {
+ String cmd = "tcp:" + remotePort + ":" + remoteAddress;
+ cmd = String.format("%04X", cmd.length()) + cmd;
+
+ byte[] buf = new byte[ADB_RESPONSE_SIZE];
+ outputStream.write(cmd.getBytes());
+ int read = inputStream.read(buf);
+ if (read != ADB_RESPONSE_SIZE || !ADB_OK.equals(new String(buf))) {
+ Log.w(LOG_TAG, "adb cmd failed.");
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ConnectionHandler.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ConnectionHandler.java
new file mode 100644
index 0000000..f19cd41
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ConnectionHandler.java
@@ -0,0 +1,172 @@
+/*
+ * 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.forwarder;
+
+import android.util.Log;
+
+import com.android.dumprendertree2.FsUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * Worker class for {@link Forwarder}. A ConnectionHandler will be created once the Forwarder
+ * accepts an incoming connection, and it will then forward the incoming/outgoing streams to a
+ * connection already proxied by adb networking (see also {@link AdbUtils}).
+ */
+public class ConnectionHandler {
+
+ private static final String LOG_TAG = "ConnectionHandler";
+
+ public static interface OnFinishedCallback {
+ public void onFinished();
+ }
+
+ private class SocketPipeThread extends Thread {
+
+ private InputStream mInputStream;
+ private OutputStream mOutputStream;
+
+ public SocketPipeThread(InputStream inputStream, OutputStream outputStream) {
+ mInputStream = inputStream;
+ mOutputStream = outputStream;
+ setName("SocketPipeThread: " + getName());
+ }
+
+ @Override
+ public void run() {
+ byte[] buffer = new byte[4096];
+ int length;
+ while (true) {
+ try {
+ if ((length = mInputStream.read(buffer)) < 0) {
+ break;
+ }
+ mOutputStream.write(buffer, 0, length);
+ } catch (IOException e) {
+ /** This exception means one of the streams is closed */
+ Log.v(LOG_TAG, this.toString(), e);
+ break;
+ }
+ }
+
+ synchronized (mThreadsRunning) {
+ mThreadsRunning--;
+ if (mThreadsRunning == 0) {
+ ConnectionHandler.this.stop();
+ mOnFinishedCallback.onFinished();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+ }
+
+ private Integer mThreadsRunning;
+
+ private Socket mFromSocket, mToSocket;
+ private SocketPipeThread mFromToPipe, mToFromPipe;
+ private InputStream mFromSocketInputStream, mToSocketInputStream;
+ private OutputStream mFromSocketOutputStream, mToSocketOutputStream;
+
+ private int mPort;
+ private String mRemoteMachineIpAddress;
+
+ private OnFinishedCallback mOnFinishedCallback;
+
+ public ConnectionHandler(String remoteMachineIp, int port, Socket fromSocket, Socket toSocket)
+ throws IOException {
+ mRemoteMachineIpAddress = remoteMachineIp;
+ mPort = port;
+
+ mFromSocket = fromSocket;
+ mToSocket = toSocket;
+
+ try {
+ mFromSocketInputStream = mFromSocket.getInputStream();
+ mToSocketInputStream = mToSocket.getInputStream();
+ mFromSocketOutputStream = mFromSocket.getOutputStream();
+ mToSocketOutputStream = mToSocket.getOutputStream();
+ AdbUtils.configureConnection(mToSocketInputStream, mToSocketOutputStream,
+ mRemoteMachineIpAddress, mPort);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unable to start ConnectionHandler", e);
+ closeStreams();
+ throw e;
+ }
+
+ mFromToPipe = new SocketPipeThread(mFromSocketInputStream, mToSocketOutputStream);
+ mToFromPipe = new SocketPipeThread(mToSocketInputStream, mFromSocketOutputStream);
+ }
+
+ public void registerOnConnectionHandlerFinishedCallback(OnFinishedCallback callback) {
+ mOnFinishedCallback = callback;
+ }
+
+ private void closeStreams() {
+ FsUtils.closeInputStream(mFromSocketInputStream);
+ FsUtils.closeInputStream(mToSocketInputStream);
+ FsUtils.closeOutputStream(mFromSocketOutputStream);
+ FsUtils.closeOutputStream(mToSocketOutputStream);
+ }
+
+ public void start() {
+ /** We have 2 threads running, one for each pipe, that we start here. */
+ mThreadsRunning = 2;
+ mFromToPipe.start();
+ mToFromPipe.start();
+ }
+
+ public void stop() {
+ shutdown(mFromSocket);
+ shutdown(mToSocket);
+ }
+
+ private void shutdown(Socket socket) {
+ synchronized (mFromToPipe) {
+ synchronized (mToFromPipe) {
+ /** This will stop the while loop in the run method */
+ try {
+ if (!socket.isInputShutdown()) {
+ socket.shutdownInput();
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mFromToPipe=" + mFromToPipe + " mToFromPipe=" + mToFromPipe, e);
+ }
+ try {
+ if (!socket.isOutputShutdown()) {
+ socket.shutdownOutput();
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mFromToPipe=" + mFromToPipe + " mToFromPipe=" + mToFromPipe, e);
+ }
+ try {
+ if (!socket.isClosed()) {
+ socket.close();
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mFromToPipe=" + mFromToPipe + " mToFromPipe=" + mToFromPipe, e);
+ }
+ }
+ }
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/Forwarder.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/Forwarder.java
new file mode 100644
index 0000000..ce22fa0
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/Forwarder.java
@@ -0,0 +1,132 @@
+/*
+ * 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.forwarder;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A port forwarding server. Listens on localhost on specified port and forwards the tcp
+ * communications to external socket via adb networking proxy.
+ */
+public class Forwarder extends Thread {
+ private static final String LOG_TAG = "Forwarder";
+
+ private int mPort;
+ private String mRemoteMachineIpAddress;
+
+ private ServerSocket mServerSocket;
+
+ private Set<ConnectionHandler> mConnectionHandlers = new HashSet<ConnectionHandler>();
+
+ public Forwarder(int port, String remoteMachineIpAddress) {
+ mPort = port;
+ mRemoteMachineIpAddress = remoteMachineIpAddress;
+ }
+
+ @Override
+ public void start() {
+ Log.i(LOG_TAG, "start(): Starting fowarder on port: " + mPort);
+
+ try {
+ mServerSocket = new ServerSocket(mPort);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mPort=" + mPort, e);
+ return;
+ }
+
+ super.start();
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ Socket localSocket;
+ try {
+ localSocket = mServerSocket.accept();
+ } catch (IOException e) {
+ /** This most likely means that mServerSocket is already closed */
+ Log.w(LOG_TAG, "mPort=" + mPort, e);
+ break;
+ }
+
+ Socket remoteSocket = null;
+ final ConnectionHandler connectionHandler;
+ try {
+ remoteSocket = AdbUtils.createSocket();
+ connectionHandler = new ConnectionHandler(
+ mRemoteMachineIpAddress, mPort, localSocket, remoteSocket);
+ } catch (IOException exception) {
+ try {
+ localSocket.close();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mPort=" + mPort, e);
+ }
+ if (remoteSocket != null) {
+ try {
+ remoteSocket.close();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mPort=" + mPort, e);
+ }
+ }
+ continue;
+ }
+
+ /**
+ * We have to close the sockets after the ConnectionHandler finishes, so we
+ * don't get "Too may open files" exception. We also remove the ConnectionHandler
+ * from the collection to avoid memory issues.
+ * */
+ ConnectionHandler.OnFinishedCallback callback =
+ new ConnectionHandler.OnFinishedCallback() {
+ @Override
+ public void onFinished() {
+ synchronized (this) {
+ if (!mConnectionHandlers.remove(connectionHandler)) {
+ assert false : "removeConnectionHandler(): not in the collection";
+ }
+ }
+ }
+ };
+ connectionHandler.registerOnConnectionHandlerFinishedCallback(callback);
+
+ synchronized (this) {
+ mConnectionHandlers.add(connectionHandler);
+ }
+ connectionHandler.start();
+ }
+
+ synchronized (this) {
+ for (ConnectionHandler connectionHandler : mConnectionHandlers) {
+ connectionHandler.stop();
+ }
+ }
+ }
+
+ public void finish() {
+ try {
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "mPort=" + mPort, e);
+ }
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ForwarderManager.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ForwarderManager.java
new file mode 100644
index 0000000..cff436f
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/forwarder/ForwarderManager.java
@@ -0,0 +1,123 @@
+/*
+ * 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.forwarder;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A simple class to start and stop Forwarders running on some ports.
+ *
+ * It uses a singleton pattern and is thread safe.
+ */
+public class ForwarderManager {
+ private static final String LOG_TAG = "ForwarderManager";
+
+ /**
+ * The IP address of the server serving the tests.
+ */
+ private static final String HOST_IP = "127.0.0.1";
+
+ /**
+ * We use these ports because other webkit platforms do. They are set up in
+ * external/webkit/LayoutTests/http/conf/apache2-debian-httpd.conf
+ */
+ public static final int HTTP_PORT = 8000;
+ public static final int HTTPS_PORT = 8443;
+
+ private static ForwarderManager forwarderManager;
+
+ private Set<Forwarder> mForwarders;
+ private boolean mIsStarted;
+
+ private ForwarderManager() {
+ mForwarders = new HashSet<Forwarder>(2);
+ mForwarders.add(new Forwarder(HTTP_PORT, HOST_IP));
+ mForwarders.add(new Forwarder(HTTPS_PORT, HOST_IP));
+ }
+
+ /**
+ * Returns the main part of the URL with the trailing slash
+ *
+ * @param isHttps
+ * @return
+ */
+ public static final String getHostSchemePort(boolean isHttps) {
+ int port;
+ String protocol;
+ if (isHttps) {
+ protocol = "https";
+ port = HTTPS_PORT;
+ } else {
+ protocol = "http";
+ port = HTTP_PORT;
+ }
+
+ URL url = null;
+ try {
+ url = new URL(protocol, HOST_IP, port, "/");
+ } catch (MalformedURLException e) {
+ assert false : "isHttps=" + isHttps;
+ }
+
+ return url.toString();
+ }
+
+ public static synchronized ForwarderManager getForwarderManager() {
+ if (forwarderManager == null) {
+ forwarderManager = new ForwarderManager();
+ }
+ return forwarderManager;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ throw new CloneNotSupportedException();
+ }
+
+ public synchronized void start() {
+ if (mIsStarted) {
+ Log.w(LOG_TAG, "start(): ForwarderManager already running! NOOP.");
+ return;
+ }
+
+ for (Forwarder forwarder : mForwarders) {
+ forwarder.start();
+ }
+
+ mIsStarted = true;
+ Log.i(LOG_TAG, "ForwarderManager started.");
+ }
+
+ public synchronized void stop() {
+ if (!mIsStarted) {
+ Log.w(LOG_TAG, "stop(): ForwarderManager already stopped! NOOP.");
+ return;
+ }
+
+ for (Forwarder forwarder : mForwarders) {
+ forwarder.finish();
+ }
+
+ mIsStarted = false;
+ Log.i(LOG_TAG, "ForwarderManager stopped.");
+ }
+}
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/OnEverythingFinishedCallback.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/OnEverythingFinishedCallback.java
new file mode 100644
index 0000000..e1d4364
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/OnEverythingFinishedCallback.java
@@ -0,0 +1,25 @@
+/*
+ * 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.scriptsupport;
+
+/**
+ * Callback used to inform scriptsupport.Starter that everything is finished and
+ * we can exit
+ */
+public interface OnEverythingFinishedCallback {
+ public void onFinished();
+} \ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/ScriptTestRunner.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/ScriptTestRunner.java
new file mode 100644
index 0000000..78f58d5
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/ScriptTestRunner.java
@@ -0,0 +1,37 @@
+/*
+ * 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.scriptsupport;
+
+import android.os.Bundle;
+import android.test.InstrumentationTestRunner;
+
+/**
+ * Extends InstrumentationTestRunner to allow the script to pass arguments to the application
+ */
+public class ScriptTestRunner extends InstrumentationTestRunner {
+ String mTestsRelativePath;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ mTestsRelativePath = arguments.getString("path");
+ super.onCreate(arguments);
+ }
+
+ public String getTestsRelativePath() {
+ return mTestsRelativePath;
+ }
+} \ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/Starter.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/Starter.java
new file mode 100644
index 0000000..6f41a0f
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/Starter.java
@@ -0,0 +1,79 @@
+/*
+ * 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.scriptsupport;
+
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.dumprendertree2.TestsListActivity;
+import com.android.dumprendertree2.forwarder.ForwarderManager;
+
+/**
+ * A class which provides methods that can be invoked by a script running on the host machine to
+ * run the tests.
+ *
+ * It starts a TestsListActivity and does not return until all the tests finish executing.
+ */
+public class Starter extends ActivityInstrumentationTestCase2<TestsListActivity> {
+ private static final String LOG_TAG = "Starter";
+ private boolean mEverythingFinished;
+
+ public Starter() {
+ super(TestsListActivity.class);
+ }
+
+ /**
+ * This method is called from adb to start executing the tests. It doesn't return
+ * until everything is finished so that the script can wait for the end if it needs
+ * to.
+ */
+ public void startLayoutTests() {
+ ScriptTestRunner runner = (ScriptTestRunner)getInstrumentation();
+ String relativePath = runner.getTestsRelativePath();
+
+ ForwarderManager.getForwarderManager().start();
+
+ Intent intent = new Intent();
+ intent.setClassName("com.android.dumprendertree2", "TestsListActivity");
+ intent.setAction(Intent.ACTION_RUN);
+ intent.putExtra(TestsListActivity.EXTRA_TEST_PATH, relativePath);
+ setActivityIntent(intent);
+ getActivity().registerOnEverythingFinishedCallback(new OnEverythingFinishedCallback() {
+ /** This method is safe to call on any thread */
+ @Override
+ public void onFinished() {
+ synchronized (Starter.this) {
+ mEverythingFinished = true;
+ Starter.this.notifyAll();
+ }
+ }
+ });
+
+ synchronized (this) {
+ while (!mEverythingFinished) {
+ try {
+ this.wait();
+ } catch (InterruptedException e) {
+ Log.e(LOG_TAG, "startLayoutTests()", e);
+ }
+ }
+ }
+
+ ForwarderManager.getForwarderManager().stop();
+ }
+} \ No newline at end of file
diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/ui/DirListActivity.java b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ui/DirListActivity.java
new file mode 100644
index 0000000..5de69a7
--- /dev/null
+++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ui/DirListActivity.java
@@ -0,0 +1,426 @@
+/*
+ * 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.ui;
+
+import com.android.dumprendertree2.FileFilter;
+import com.android.dumprendertree2.FsUtils;
+import com.android.dumprendertree2.TestsListActivity;
+import com.android.dumprendertree2.R;
+import com.android.dumprendertree2.forwarder.ForwarderManager;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ListActivity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An Activity that allows navigating through tests folders and choosing folders or tests to run.
+ */
+public class DirListActivity extends ListActivity {
+
+ private static final String LOG_TAG = "DirListActivity";
+
+ /** TODO: This is just a guess - think of a better way to achieve it */
+ private static final int MEAN_TITLE_CHAR_SIZE = 13;
+
+ private static final int PROGRESS_DIALOG_DELAY_MS = 200;
+
+ /** Code for the dialog, used in showDialog and onCreateDialog */
+ private static final int DIALOG_RUN_ABORT_DIR = 0;
+
+ /** Messages codes */
+ private static final int MSG_LOADED_ITEMS = 0;
+ private static final int MSG_SHOW_PROGRESS_DIALOG = 1;
+
+ private static final CharSequence NO_RESPONSE_MESSAGE =
+ "No response from host when getting directory contents. Is the host server running?";
+
+ /** Initialized lazily before first sProgressDialog.show() */
+ private static ProgressDialog sProgressDialog;
+
+ private ListView mListView;
+
+ /** This is a relative path! */
+ private String mCurrentDirPath;
+
+ /**
+ * A thread responsible for loading the contents of the directory from sd card
+ * and sending them via Message to main thread that then loads them into
+ * ListView
+ */
+ private class LoadListItemsThread extends Thread {
+ private Handler mHandler;
+ private String mRelativePath;
+
+ public LoadListItemsThread(String relativePath, Handler handler) {
+ mRelativePath = relativePath;
+ mHandler = handler;
+ }
+
+ @Override
+ public void run() {
+ Message msg = mHandler.obtainMessage(MSG_LOADED_ITEMS);
+ msg.obj = getDirList(mRelativePath);
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ /**
+ * Very simple object to use inside ListView as an item.
+ */
+ private static class ListItem implements Comparable<ListItem> {
+ private String mRelativePath;
+ private String mName;
+ private boolean mIsDirectory;
+
+ public ListItem(String relativePath, boolean isDirectory) {
+ mRelativePath = relativePath;
+ mName = new File(relativePath).getName();
+ mIsDirectory = isDirectory;
+ }
+
+ public boolean isDirectory() {
+ return mIsDirectory;
+ }
+
+ public String getRelativePath() {
+ return mRelativePath;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public int compareTo(ListItem another) {
+ return mRelativePath.compareTo(another.getRelativePath());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ListItem)) {
+ return false;
+ }
+
+ return mRelativePath.equals(((ListItem)o).getRelativePath());
+ }
+
+ @Override
+ public int hashCode() {
+ return mRelativePath.hashCode();
+ }
+
+ }
+
+ /**
+ * A custom adapter that sets the proper icon and label in the list view.
+ */
+ private static class DirListAdapter extends ArrayAdapter<ListItem> {
+ private Activity mContext;
+ private ListItem[] mItems;
+
+ public DirListAdapter(Activity context, ListItem[] items) {
+ super(context, R.layout.dirlist_row, items);
+
+ mContext = context;
+ mItems = items;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = mContext.getLayoutInflater();
+ View row = inflater.inflate(R.layout.dirlist_row, null);
+
+ TextView label = (TextView)row.findViewById(R.id.label);
+ label.setText(mItems[position].getName());
+
+ ImageView icon = (ImageView)row.findViewById(R.id.icon);
+ if (mItems[position].isDirectory()) {
+ icon.setImageResource(R.drawable.folder);
+ } else {
+ icon.setImageResource(R.drawable.runtest);
+ }
+
+ return row;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ForwarderManager.getForwarderManager().start();
+
+ mListView = getListView();
+
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ ListItem item = (ListItem)parent.getItemAtPosition(position);
+
+ if (item.isDirectory()) {
+ showDir(item.getRelativePath());
+ } else {
+ /** Run the test */
+ runAllTestsUnder(item.getRelativePath());
+ }
+ }
+ });
+
+ mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ ListItem item = (ListItem)parent.getItemAtPosition(position);
+
+ if (item.isDirectory()) {
+ Bundle arguments = new Bundle(1);
+ arguments.putString("name", item.getName());
+ arguments.putString("relativePath", item.getRelativePath());
+ showDialog(DIALOG_RUN_ABORT_DIR, arguments);
+ } else {
+ /** TODO: Maybe show some info about a test? */
+ }
+
+ return true;
+ }
+ });
+
+ /** All the paths are relative to test root dir where possible */
+ showDir("");
+ }
+
+ private void runAllTestsUnder(String relativePath) {
+ Intent intent = new Intent();
+ intent.setClass(DirListActivity.this, TestsListActivity.class);
+ intent.setAction(Intent.ACTION_RUN);
+ intent.putExtra(TestsListActivity.EXTRA_TEST_PATH, relativePath);
+ startActivity(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.gui_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.run_all:
+ runAllTestsUnder(mCurrentDirPath);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ /**
+ * Moves to the parent directory if one exists. Does not allow to move above
+ * the test 'root' directory.
+ */
+ public void onBackPressed() {
+ File currentDirParent = new File(mCurrentDirPath).getParentFile();
+ if (currentDirParent != null) {
+ showDir(currentDirParent.getPath());
+ } else {
+ showDir("");
+ }
+ }
+
+ /**
+ * Prevents the activity from recreating on change of orientation. The title needs to
+ * be recalculated.
+ */
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ setTitle(shortenTitle(mCurrentDirPath));
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, final Bundle args) {
+ Dialog dialog = null;
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+
+ switch (id) {
+ case DIALOG_RUN_ABORT_DIR:
+ builder.setTitle(getText(R.string.dialog_run_abort_dir_title_prefix) + " " +
+ args.getString("name"));
+ builder.setMessage(R.string.dialog_run_abort_dir_msg);
+ builder.setCancelable(true);
+
+ builder.setPositiveButton(R.string.dialog_run_abort_dir_ok_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ removeDialog(DIALOG_RUN_ABORT_DIR);
+ runAllTestsUnder(args.getString("relativePath"));
+ }
+ });
+
+ builder.setNegativeButton(R.string.dialog_run_abort_dir_abort_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ removeDialog(DIALOG_RUN_ABORT_DIR);
+ }
+ });
+
+ dialog = builder.create();
+ dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ removeDialog(DIALOG_RUN_ABORT_DIR);
+ }
+ });
+ break;
+ }
+
+ return dialog;
+ }
+
+ /**
+ * Loads the contents of dir into the list view.
+ *
+ * @param dirPath
+ * directory to load into list view
+ */
+ private void showDir(String dirPath) {
+ mCurrentDirPath = dirPath;
+
+ /** Show progress dialog with a delay */
+ final Handler delayedDialogHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_SHOW_PROGRESS_DIALOG) {
+ if (sProgressDialog == null) {
+ sProgressDialog = new ProgressDialog(DirListActivity.this);
+ sProgressDialog.setCancelable(false);
+ sProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ sProgressDialog.setTitle(R.string.dialog_progress_title);
+ sProgressDialog.setMessage(getText(R.string.dialog_progress_msg));
+ }
+ sProgressDialog.show();
+ }
+ }
+ };
+ Message msgShowDialog = delayedDialogHandler.obtainMessage(MSG_SHOW_PROGRESS_DIALOG);
+ delayedDialogHandler.sendMessageDelayed(msgShowDialog, PROGRESS_DIALOG_DELAY_MS);
+
+ /** Delegate loading contents from SD card to a new thread */
+ new LoadListItemsThread(mCurrentDirPath, new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_LOADED_ITEMS) {
+ setTitle(shortenTitle(mCurrentDirPath));
+ delayedDialogHandler.removeMessages(MSG_SHOW_PROGRESS_DIALOG);
+ if (sProgressDialog != null) {
+ sProgressDialog.dismiss();
+ }
+ if (msg.obj == null) {
+ Toast.makeText(DirListActivity.this, NO_RESPONSE_MESSAGE,
+ Toast.LENGTH_LONG).show();
+ } else {
+ setListAdapter(new DirListAdapter(DirListActivity.this,
+ (ListItem[])msg.obj));
+ }
+ }
+ }
+ }).start();
+ }
+
+ /**
+ * TODO: find a neat way to determine number of characters that fit in the title
+ * bar.
+ * */
+ private String shortenTitle(String title) {
+ if (title.equals("")) {
+ return "Tests' root dir:";
+ }
+ int charCount = mListView.getWidth() / MEAN_TITLE_CHAR_SIZE;
+
+ if (title.length() > charCount) {
+ return "..." + title.substring(title.length() - charCount);
+ } else {
+ return title;
+ }
+ }
+
+ /**
+ * Return the array with contents of the given directory.
+ * First it contains the subfolders, then the files. Both sorted
+ * alphabetically.
+ *
+ * The dirPath is relative.
+ */
+ private ListItem[] getDirList(String dirPath) {
+ List<ListItem> subDirs = new ArrayList<ListItem>();
+ List<ListItem> subFiles = new ArrayList<ListItem>();
+
+ List<String> dirRelativePaths = FsUtils.getLayoutTestsDirContents(dirPath, false, true);
+ if (dirRelativePaths == null) {
+ return null;
+ }
+ for (String dirRelativePath : dirRelativePaths) {
+ if (FileFilter.isTestDir(new File(dirRelativePath).getName())) {
+ subDirs.add(new ListItem(dirRelativePath, true));
+ }
+ }
+
+ List<String> testRelativePaths = FsUtils.getLayoutTestsDirContents(dirPath, false, false);
+ if (testRelativePaths == null) {
+ return null;
+ }
+ for (String testRelativePath : testRelativePaths) {
+ if (FileFilter.isTestFile(new File(testRelativePath).getName())) {
+ subFiles.add(new ListItem(testRelativePath, false));
+ }
+ }
+
+ /** Concatenate the two lists */
+ subDirs.addAll(subFiles);
+
+ return subDirs.toArray(new ListItem[subDirs.size()]);
+ }
+}