diff options
Diffstat (limited to 'tests/DumpRenderTree2')
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 Binary files differnew file mode 100644 index 0000000..5b3fcec --- /dev/null +++ b/tests/DumpRenderTree2/res/drawable/folder.png diff --git a/tests/DumpRenderTree2/res/drawable/runtest.png b/tests/DumpRenderTree2/res/drawable/runtest.png Binary files differnew file mode 100644 index 0000000..910c654 --- /dev/null +++ b/tests/DumpRenderTree2/res/drawable/runtest.png 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 = '▼ ';" + + " } else {" + + " element.style.display = 'none';" + + " triangle.innerHTML = '▶ ';" + + " }" + + " }" + + "</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 + "\">▶ </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("\">■ </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 = " "; + + if (diff.text.endsWith("\n") || forceOutputLine) { + lineNums.add(i); + /** TODO: Think of better way to replace stuff */ + line += begSpan + diff.text.replace(" ", " ") + + endSpan + br; + lines.add(line); + line = ""; + } else { + line += begSpan + diff.text.replace(" ", " ") + 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()]); + } +} |