diff options
Diffstat (limited to 'tests/DumpRenderTree2')
27 files changed, 4330 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..dd0c4e9 --- /dev/null +++ b/tests/DumpRenderTree2/AndroidManifest.xml @@ -0,0 +1,58 @@ +<?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.LAUNCHER" /> + </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"> + </activity> + + <activity android:name=".LayoutTestsExecutor" + 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>
\ No newline at end of file diff --git a/tests/DumpRenderTree2/assets/run_apache2.py b/tests/DumpRenderTree2/assets/run_apache2.py new file mode 100755 index 0000000..f404090 --- /dev/null +++ b/tests/DumpRenderTree2/assets/run_apache2.py @@ -0,0 +1,107 @@ +#!/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 + +def main(): + if len(sys.argv) < 2: + run_cmd = "" + else: + run_cmd = sys.argv[1] + + #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) + + #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") + + #Prepare the command to (re)start/stop the server with specified settings + apache2_restart_cmd = "apache2 -k " + run_cmd + directives = " -c \"ServerRoot " + android_tree_root + "\"" + directives += " -c \"DocumentRoot " + os.path.join("external", "webkit") + "\"" + + #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 \"SSLCertificateFile " + os.path.join ("external", "webkit", "LayoutTests", + "http", "conf", "webkit-httpd.pem") + "\"" + directives += " -c \"User ${APACHE_RUN_USER}\"" + directives += " -c \"Group ${APACHE_RUN_GROUP}\"" + directives += " -C \"TypesConfig " + os.path.join("/etc", "mime.types") + "\"" + conf_file_cmd = " -f " + os.path.join(android_tree_root, "external", "webkit", "LayoutTests", + "http", "conf", "apache2-debian-httpd.conf") + + #Try to execute the commands + logging.info("Will " + run_cmd + " apache2 server.") + cmd = export_envvars_cmd + " && " + apache2_restart_cmd + directives + conf_file_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__": + main(); diff --git a/tests/DumpRenderTree2/assets/run_layout_tests.py b/tests/DumpRenderTree2/assets/run_layout_tests.py new file mode 100755 index 0000000..b13d8c9 --- /dev/null +++ b/tests/DumpRenderTree2/assets/run_layout_tests.py @@ -0,0 +1,65 @@ +#!/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 opens html details in the default browser. + + Usage: + run_layout_tests.py PATH +""" + +import sys +import os +import subprocess +import logging +import webbrowser +import tempfile + +#TODO: These should not be hardcoded +RESULTS_ABSOLUTE_PATH = "/sdcard/android/LayoutTests-results/" +DETAILS_HTML = "details.html" +SUMMARY_TXT = "summary.txt" + +def main(): + if len(sys.argv) > 1: + path = sys.argv[1] + else: + path = "" + + logging.basicConfig(level=logging.INFO, format='%(message)s') + + tmpdir = tempfile.gettempdir() + + # Run the tests in path + cmd = "adb 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...") + subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + + logging.info("Downloading the summaries...") + + # Download the txt summary to tmp folder + summary_txt_tmp_path = os.path.join(tmpdir, SUMMARY_TXT) + cmd = "adb pull " + RESULTS_ABSOLUTE_PATH + SUMMARY_TXT + " " + summary_txt_tmp_path + subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + + # Download the html summary to tmp folder + details_html_tmp_path = os.path.join(tmpdir, DETAILS_HTML) + cmd = "adb pull " + RESULTS_ABSOLUTE_PATH + DETAILS_HTML + " " + details_html_tmp_path + subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + + # 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 + webbrowser.open(details_html_tmp_path) + +if __name__ == "__main__": + main(); 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/values/strings.xml b/tests/DumpRenderTree2/res/values/strings.xml new file mode 100644 index 0000000..5fd1eb9 --- /dev/null +++ b/tests/DumpRenderTree2/res/values/strings.xml @@ -0,0 +1,28 @@ +<?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> +</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..d68930c --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/AbstractResult.java @@ -0,0 +1,145 @@ +/* + * 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 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> { + + 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); + } + + public enum ResultCode { + PASS("Passed"), + FAIL_RESULT_DIFFERS("Result differs"), + FAIL_NO_EXPECTED_RESULT("No expected result"), + FAIL_TIMED_OUT("Timed out"), + FAIL_CRASHED("Crashed"); + + 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; + } + + /** + * 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 setExpectedTextResult(String expectedResult); + + /** + * 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 code of this result. + * + * @return + * the code of this result + */ + public abstract ResultCode getResultCode(); + + /** + * 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(); +}
\ No newline at end of file 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..8fca629 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/AdditionalTextOutput.java @@ -0,0 +1,118 @@ +/* + * 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 + "::appendDatabaseCallback", e.getMessage()); + } + + 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..a793dab --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/CrashedDummyResult.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dumprendertree2; + +import android.os.Bundle; +import android.os.Message; +import android.webkit.WebView; + +/** + * A dummy class representing test that crashed. + */ +public class CrashedDummyResult extends AbstractResult { + String mRelativePath; + + public CrashedDummyResult(String relativePath) { + mRelativePath = relativePath; + } + + @Override + public byte[] getActualImageResult() { + return null; + } + + @Override + public String getActualTextResult() { + return null; + } + + @Override + public Bundle getBundle() { + /** TODO: */ + return null; + } + + @Override + public String getDiffAsHtml() { + /** TODO: Probably show at least expected results */ + return "Ooops, I crashed..."; + } + + @Override + public String getRelativePath() { + return mRelativePath; + } + + @Override + public ResultCode getResultCode() { + return ResultCode.FAIL_CRASHED; + } + + @Override + public TestType getType() { + return null; + } + + @Override + public void obtainActualResults(WebView webview, Message resultObtainedMsg) { + /** This method is not applicable for this type of result */ + assert false; + } + + @Override + public void setExpectedImageResult(byte[] expectedResult) { + /** TODO */ + } + + @Override + public void setExpectedTextResult(String expectedResult) { + /** TODO */ + } +}
\ No newline at end of file diff --git a/tests/DumpRenderTree2/src/com/android/dumprendertree2/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..93e6137 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/EventSenderImpl.java @@ -0,0 +1,563 @@ +/* + * 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 mX; + private int mY; + private long mDownTime; + private boolean mReleased = false; + private boolean mMoved = false; + private boolean mCancelled = false; + + public TouchPoint(WebView webView, int x, int y) { + mWebView = webView; + mX = scaleX(x); + mY = scaleY(y); + } + + 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) { + TouchPoint touchPoint; + Bundle bundle; + KeyEvent event; + + 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: + /** TODO: Implement */ + break; + + case MSG_MOUSE_UP: + /** TODO: Implement */ + break; + + case MSG_MOUSE_CLICK: + /** TODO: Implement */ + break; + + case MSG_MOUSE_MOVE_TO: + int x = msg.arg1; + int y = msg.arg2; + + event = null; + if (x > mMouseX) { + event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); + } else if (x < mMouseX) { + event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); + } + if (event != null) { + mWebView.onKeyDown(event.getKeyCode(), event); + mWebView.onKeyUp(event.getKeyCode(), event); + } + + event = null; + if (y > mMouseY) { + event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); + } else if (y < mMouseY) { + event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP); + } + if (event != null) { + mWebView.onKeyDown(event.getKeyCode(), event); + mWebView.onKeyUp(event.getKeyCode(), event); + } + + mMouseX = x; + mMouseY = y; + break; + + /** TOUCH */ + + case MSG_ADD_TOUCH_POINT: + getTouchPoints().add(new TouchPoint(mWebView, + msg.arg1, msg.arg2)); + if (getTouchPoints().size() > 1) { + Log.w(LOG_TAG + "::MSG_ADD_TOUCH_POINT", "Added more than one touch point"); + } + break; + + case MSG_TOUCH_START: + /** + * 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; + } + touchPoint = getTouchPoints().get(0); + + touchPoint.setDownTime(SystemClock.uptimeMillis()); + executeTouchEvent(touchPoint, MotionEvent.ACTION_DOWN); + break; + + case MSG_UPDATE_TOUCH_POINT: + bundle = (Bundle)msg.obj; + + int id = bundle.getInt("id"); + if (id >= getTouchPoints().size()) { + Log.w(LOG_TAG + "::MSG_UPDATE_TOUCH_POINT", "TouchPoint out of bounds: " + + id); + break; + } + + getTouchPoints().get(id).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; + } + touchPoint = getTouchPoints().get(0); + + if (!touchPoint.hasMoved()) { + return; + } + executeTouchEvent(touchPoint, MotionEvent.ACTION_MOVE); + touchPoint.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; + } + touchPoint = getTouchPoints().get(0); + + if (touchPoint.isCancelled()) { + executeTouchEvent(touchPoint, 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; + } + touchPoint = getTouchPoints().get(0); + + executeTouchEvent(touchPoint, MotionEvent.ACTION_UP); + if (touchPoint.isReleased()) { + getTouchPoints().remove(0); + touchPoint = null; + } + 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(TouchPoint touchPoint, int action) { + MotionEvent event = + MotionEvent.obtain(touchPoint.getDownTime(), SystemClock.uptimeMillis(), + action, touchPoint.getX(), touchPoint.getY(), mTouchMetaState); + 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; + } +}
\ No newline at end of file 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..cf82d24 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/FileFilter.java @@ -0,0 +1,288 @@ +/* + * 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 java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +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 TOKEN_SKIP = "SKIP"; + private static final String TOKEN_IGNORE_RESULT = "IGNORE_RESULT"; + private static final String TOKEN_SLOW = "SLOW"; + + private final Set<String> mSkipList = new HashSet<String>(); + private final Set<String> mIgnoreResultList = new HashSet<String>(); + private final Set<String> mSlowList = new HashSet<String>(); + + private final String mRootDirPath; + + public FileFilter(String rootDirPath) { + /** It may or may not contain a trailing slash */ + this.mRootDirPath = rootDirPath; + + reloadConfiguration(); + } + + private static final String trimTrailingSlashIfPresent(String path) { + File file = new File(path); + return file.getPath(); + } + + public void reloadConfiguration() { + File txt_exp = new File(mRootDirPath, TEST_EXPECTATIONS_TXT_PATH); + + BufferedReader bufferedReader; + try { + bufferedReader = + new BufferedReader(new FileReader(txt_exp)); + + String line; + String entry; + String[] parts; + String path; + Set<String> tokens; + Boolean skipped; + 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 */ + skipped = false; + if (tokens.contains(TOKEN_SKIP)) { + mSkipList.add(path); + skipped = true; + } + + /** If test is on skip list we ignore any further options */ + if (skipped) { + continue; + } + + if (tokens.contains(TOKEN_IGNORE_RESULT)) { + mIgnoreResultList.add(path); + } + + if (tokens.contains(TOKEN_SLOW)) { + mSlowList.add(path); + } + } + } catch (FileNotFoundException e) { + Log.w(LOG_TAG + "::reloadConfiguration", "File not found: " + txt_exp.getPath()); + } catch (IOException e) { + Log.e(LOG_TAG + "::reloadConfiguration", "IOException: " + e.getMessage()); + } + } + + /** + * Checks if test is supposed to be skipped. + * + * <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 isSkip(String testPath) { + for (String prefix : getPrefixes(testPath)) { + if (mSkipList.contains(prefix)) { + return true; + } + } + + return false; + } + + /** + * Checks if test result is supposed to be ignored. + * + * <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 ignored + */ + public boolean isIgnoreRes(String testPath) { + for (String prefix : getPrefixes(testPath)) { + if (mIgnoreResultList.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 the path to the file relative to the tests root dir + * + * @param filePath + * @return + * the path relative to the tests root dir + */ + public String getRelativePath(String filePath) { + File rootDir = new File(mRootDirPath); + return filePath.replaceFirst(rootDir.getPath() + File.separator, ""); + } + + /** + * Return the path to the file relative to the tests root dir + * + * @param filePath + * @return + * the path relative to the tests root dir + */ + public String getRelativePath(File file) { + return getRelativePath(file.getAbsolutePath()); + } + + public File getAbsoluteFile(String relativePath) { + return new File(mRootDirPath, relativePath); + } + + public String getAboslutePath(String relativePath) { + return getAbsoluteFile(relativePath).getAbsolutePath(); + } + + /** + * 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; + } +}
\ No newline at end of file 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..212c187 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/FsUtils.java @@ -0,0 +1,78 @@ +/* + * 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 java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * + */ +public class FsUtils { + public static final String LOG_TAG = "FsUtils"; + + 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."); + outputStream = new FileOutputStream(file, append); + outputStream.write(bytes); + } finally { + if (outputStream != null) { + outputStream.close(); + } + } + } catch (IOException e) { + Log.e(LOG_TAG + "::writeDataToStorage", e.getMessage()); + } + } + + 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 + "::readDataFromStorage", e.getMessage()); + } + + return bytes; + } +}
\ No newline at end of file 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..6db9571 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestController.java @@ -0,0 +1,100 @@ +/* + * 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 waitUntilDone() { + mLayoutTestsExecutor.waitUntilDone(); + } + + public void notifyDone() { + mLayoutTestsExecutor.notifyDone(); + } + + public void dumpAsText() { + dumpAsText(false); + } + + public void dumpAsText(boolean enablePixelTest) { + mLayoutTestsExecutor.dumpAsText(enablePixelTest); + } + + public void dumpChildFramesAsText() { + mLayoutTestsExecutor.dumpChildFramesAsText(); + } + + public void clearAllDatabases() { + Log.w(LOG_TAG + "::clearAllDatabases", "called"); + WebStorage.getInstance().deleteAllData(); + } + + public void setCanOpenWindows() { + mLayoutTestsExecutor.setCanOpenWindows(); + } + + public void dumpDatabaseCallbacks() { + mLayoutTestsExecutor.dumpDatabaseCallbacks(); + } + + public void setDatabaseQuota(long quota) { + /** TODO: Reset this before every test! */ + Log.w(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 setMockGeolocationPosition(double latitude, double longitude, double accuracy) { + Log.w(LOG_TAG + "::setMockGeolocationPosition", "latitude: " + latitude + + " longitude: " + longitude + " accuracy: " + accuracy); + MockGeolocation.getInstance().setPosition(latitude, longitude, accuracy); + } + + public void setMockGeolocationError(int code, String message) { + Log.w(LOG_TAG + "::setMockGeolocationError", "code: " + code + " message: " + message); + MockGeolocation.getInstance().setError(code, message); + } + + 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. + mLayoutTestsExecutor.setMockDeviceOrientation( + canProvideAlpha, alpha, canProvideBeta, beta, canProvideGamma, gamma); + } +} 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..4737657 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/LayoutTestsExecutor.java @@ -0,0 +1,603 @@ +/* + * 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.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.RemoteException; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.view.Window; +import android.webkit.ConsoleMessage; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.webkit.GeolocationPermissions; +import android.webkit.WebStorage.QuotaUpdater; + +import java.io.File; +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; + } + } + + /** TODO: make it a setting */ + static final String TESTS_ROOT_DIR_PATH = + Environment.getExternalStorageDirectory() + + File.separator + "android" + + File.separator + "LayoutTests"; + + private static final String LOG_TAG = "LayoutTestExecutor"; + + 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 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(); + } + } + }; + + 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 = new WebView(LayoutTestsExecutor.this); + 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); + + 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 = new WebView(this); + setupWebView(mCurrentWebView); + + mEventSender.reset(mCurrentWebView); + + setContentView(mCurrentWebView); + if (previousWebView != null) { + Log.d(LOG_TAG + "::reset", "previousWebView != null"); + previousWebView.destroy(); + } + } + + private void setupWebView(WebView webView) { + webView.setWebViewClient(mWebViewClient); + webView.setWebChromeClient(mWebChromeClient); + webView.addJavascriptInterface(mLayoutTestController, "layoutTestController"); + webView.addJavascriptInterface(mEventSender, "eventSender"); + + /** + * 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); + + WebSettings webViewSettings = webView.getSettings(); + webViewSettings.setAppCacheEnabled(true); + webViewSettings.setAppCachePath(getApplicationContext().getCacheDir().getPath()); + webViewSettings.setAppCacheMaxSize(Long.MAX_VALUE); + 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(); + } + + private void startTests() { + try { + Message serviceMsg = + Message.obtain(null, ManagerService.MSG_FIRST_TEST); + + Bundle bundle = new Bundle(); + if (!mTestsList.isEmpty()) { + bundle.putString("firstTest", mTestsList.get(0)); + bundle.putInt("index", mCurrentTestIndex); + } + + serviceMsg.setData(bundle); + mManagerServiceMessenger.send(serviceMsg); + } catch (RemoteException e) { + Log.e(LOG_TAG + "::startTests", e.getMessage()); + } + + runNextTest(); + } + + private void runNextTest() { + assert mCurrentState == CurrentState.IDLE : "mCurrentState = " + mCurrentState.name(); + + if (mTestsList.isEmpty()) { + onAllTestsFinished(); + return; + } + + mCurrentTestRelativePath = mTestsList.remove(0); + Log.d(LOG_TAG + "::runNextTest", "Start: " + mCurrentTestRelativePath + + "(" + mCurrentTestIndex + ")"); + mCurrentTestUri = + Uri.fromFile(new File(TESTS_ROOT_DIR_PATH, 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(); + + 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(); + + 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; + + mCurrentResult.obtainActualResults(mCurrentWebView, + mResultHandler.obtainMessage(MSG_ACTUAL_RESULT_OBTAINED)); + } + + private void onActualResultsObtained() { + assert mCurrentState == CurrentState.OBTAINING_RESULT + : "mCurrentState = " + mCurrentState.name(); + + mCurrentState = CurrentState.IDLE; + + mResultHandler.removeMessages(MSG_TEST_TIMED_OUT); + 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 (mCurrentTestTimedOut) { + bundle.putString("resultCode", AbstractResult.ResultCode.FAIL_TIMED_OUT.name()); + } + if (!mTestsList.isEmpty()) { + bundle.putString("nextTest", mTestsList.get(0)); + } + + serviceMsg.setData(bundle); + mManagerServiceMessenger.send(serviceMsg); + } catch (RemoteException e) { + Log.e(LOG_TAG + "::reportResultToService", e.getMessage()); + } + } + + 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 + "::onAllTestsFinished", e.getMessage()); + } + + 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; + + Handler mLayoutTestControllerHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); + + switch (msg.what) { + case MSG_WAIT_UNTIL_DONE: + mCurrentState = CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST; + break; + + case MSG_NOTIFY_DONE: + if (mCurrentState == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST) { + onTestFinished(); + } + break; + + 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_SET_CAN_OPEN_WINDOWS: + mCanOpenWindows = true; + break; + + case MSG_DUMP_DATABASE_CALLBACKS: + mDumpDatabaseCallbacks = true; + break; + + case MSG_SET_GEOLOCATION_PERMISSION: + mIsGeolocationPermissionSet = true; + mGeolocationPermission = msg.arg1 == 1; + + if (mPendingGeolocationPermissionCallbacks != null) { + Iterator iter = mPendingGeolocationPermissionCallbacks.keySet().iterator(); + while (iter.hasNext()) { + GeolocationPermissions.Callback callback = + (GeolocationPermissions.Callback) iter.next(); + String origin = (String) mPendingGeolocationPermissionCallbacks.get(callback); + callback.invoke(origin, mGeolocationPermission, false); + } + mPendingGeolocationPermissionCallbacks = null; + } + break; + + default: + Log.w(LOG_TAG + "::handleMessage", "Message code does not exist: " + msg.what); + break; + } + } + }; + + private void resetLayoutTestController() { + mCanOpenWindows = false; + mDumpDatabaseCallbacks = false; + mIsGeolocationPermissionSet = false; + mPendingGeolocationPermissionCallbacks = null; + } + + public void waitUntilDone() { + Log.w(LOG_TAG + "::waitUntilDone", "called"); + mLayoutTestControllerHandler.sendEmptyMessage(MSG_WAIT_UNTIL_DONE); + } + + public void notifyDone() { + Log.w(LOG_TAG + "::notifyDone", "called"); + mLayoutTestControllerHandler.sendEmptyMessage(MSG_NOTIFY_DONE); + } + + public void dumpAsText(boolean enablePixelTest) { + Log.w(LOG_TAG + "::dumpAsText(" + enablePixelTest + ")", "called"); + /** TODO: Implement */ + if (enablePixelTest) { + Log.w(LOG_TAG + "::dumpAsText", "enablePixelTest not implemented, switching to false"); + } + mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_AS_TEXT); + } + + public void dumpChildFramesAsText() { + Log.w(LOG_TAG + "::dumpChildFramesAsText", "called"); + mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_CHILD_FRAMES_AS_TEXT); + } + + public void setCanOpenWindows() { + Log.w(LOG_TAG + "::setCanOpenWindows", "called"); + mLayoutTestControllerHandler.sendEmptyMessage(MSG_SET_CAN_OPEN_WINDOWS); + } + + public void dumpDatabaseCallbacks() { + Log.w(LOG_TAG + "::dumpDatabaseCallbacks:", "called"); + mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_DATABASE_CALLBACKS); + } + + public void setGeolocationPermission(boolean allow) { + Log.w(LOG_TAG + "::setGeolocationPermission", "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) { + mCurrentWebView.setMockDeviceOrientation(canProvideAlpha, alpha, canProvideBeta, beta, + canProvideGamma, gamma); + } +} 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..31026d6 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ManagerService.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.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_TEST_CRASHED = 0; + + private static final int CRASH_TIMEOUT_MS = 20 * 1000; + + /** TODO: make it a setting */ + static final String TESTS_ROOT_DIR_PATH = + Environment.getExternalStorageDirectory() + + File.separator + "android" + + File.separator + "LayoutTests"; + + /** TODO: make it a setting */ + static final String RESULTS_ROOT_DIR_PATH = + Environment.getExternalStorageDirectory() + + File.separator + "android" + + File.separator + "LayoutTests-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; + + /** + * 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_ALL_TESTS_FINISHED: + mSummarizer.setTestsRelativePath(mAllTestsRelativePath); + mSummarizer.summarize(); + 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 Messenger mMessenger = new Messenger(mIncomingHandler); + + private Handler mCrashMessagesHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_TEST_CRASHED) { + onTestCrashed(); + } + } + }; + + private FileFilter mFileFilter; + private Summarizer mSummarizer; + + private String mCurrentlyRunningTest; + private int mCurrentlyRunningTestIndex; + + private String mAllTestsRelativePath; + + @Override + public void onCreate() { + super.onCreate(); + + mFileFilter = new FileFilter(TESTS_ROOT_DIR_PATH); + mSummarizer = new Summarizer(mFileFilter, RESULTS_ROOT_DIR_PATH); + } + + @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) { + mCrashMessagesHandler.removeMessages(MSG_TEST_CRASHED); + ensureNextTestSetup(bundle.getString("nextTest"), bundle.getInt("testIndex") + 1); + + AbstractResult results = + AbstractResult.TestType.valueOf(bundle.getString("type")).createResult(bundle); + + handleResults(results); + } + + private void ensureNextTestSetup(String nextTest, int index) { + if (nextTest == null) { + return; + } + + mCurrentlyRunningTest = nextTest; + mCurrentlyRunningTestIndex = index; + mCrashMessagesHandler.sendEmptyMessageDelayed(MSG_TEST_CRASHED, CRASH_TIMEOUT_MS); + } + + /** + * This sends an intent to TestsListActivity to restart LayoutTestsExecutor. + * The more detailed description of the flow is in the comment of onNewIntent + * method in TestsListActivity. + */ + private void onTestCrashed() { + handleResults(new CrashedDummyResult(mCurrentlyRunningTest)); + + Log.w(LOG_TAG + "::onTestCrashed", mCurrentlyRunningTest + + "(" + mCurrentlyRunningTestIndex + ")"); + + Intent intent = new Intent(this, TestsListActivity.class); + intent.setAction(Intent.ACTION_REBOOT); + /** 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.setExpectedImageResult(getExpectedImageResult(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 static String getExpectedTextResult(String relativePath) { + byte[] result = getExpectedResult(relativePath, TEXT_RESULT_EXTENSION); + if (result != null) { + return new String(result); + } + return null; + } + + public static byte[] getExpectedImageResult(String relativePath) { + return getExpectedResult(relativePath, IMAGE_RESULT_EXTENSION); + } + + private static byte[] getExpectedResult(String relativePath, String extension) { + String originalRelativePath = + FileFilter.setPathEnding(relativePath, "-expected." + extension); + + 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.readDataFromStorage(new File(TESTS_ROOT_DIR_PATH, relativePath)); + } + + return bytes; + } +}
\ No newline at end of file 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..43bc0b1 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/Summarizer.java @@ -0,0 +1,391 @@ +/* + * 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.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.util.DisplayMetrics; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +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-bottom: 4px;}" + + "h3 a span.path {" + + " text-decoration: underline;}" + + "h3 span.tri {" + + " text-decoration: none;" + + " float: left;" + + " width: 20px;}" + + "h3 span.sqr {" + + " text-decoration: none;" + + " color: #8ee100;" + + " float: left;" + + " width: 20px;}" + + "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; }" + + "span.fail {" + + " color: red;}" + + "span.pass {" + + " color: green;}" + + "span.time_out {" + + " color: orange;}" + + "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.PASS.name() + "{" + + " background-color: #8ee100;" + + " color: black;}" + + "span." + AbstractResult.ResultCode.FAIL_RESULT_DIFFERS.name() + "{" + + " background-color: #ccc;" + + " color: black;}" + + "span." + AbstractResult.ResultCode.FAIL_NO_EXPECTED_RESULT.name() + "{" + + " background-color: #a700e4;" + + " color: #fff;}" + + "span." + AbstractResult.ResultCode.FAIL_TIMED_OUT.name() + "{" + + " background-color: #f3cb00;" + + " color: black;}" + + "span." + AbstractResult.ResultCode.FAIL_CRASHED.name() + "{" + + " 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 int mCrashedTestsCount = 0; + private List<AbstractResult> mFailedNotIgnoredTests = new ArrayList<AbstractResult>(); + private List<AbstractResult> mIgnoredTests = new ArrayList<AbstractResult>(); + private List<String> mPassedNotIgnoredTests = new ArrayList<String>(); + + private FileFilter mFileFilter; + private String mResultsRootDirPath; + + private String mTestsRelativePath; + + private Date mDate; + + public Summarizer(FileFilter fileFilter, String resultsRootDirPath) { + mFileFilter = fileFilter; + mResultsRootDirPath = resultsRootDirPath; + } + + public void appendTest(AbstractResult result) { + String relativePath = result.getRelativePath(); + + if (result.getResultCode() == AbstractResult.ResultCode.FAIL_CRASHED) { + mCrashedTestsCount++; + } + + if (mFileFilter.isIgnoreRes(relativePath)) { + mIgnoredTests.add(result); + } else if (result.getResultCode() == AbstractResult.ResultCode.PASS) { + mPassedNotIgnoredTests.add(relativePath); + } else { + mFailedNotIgnoredTests.add(result); + } + } + + public void setTestsRelativePath(String testsRelativePath) { + mTestsRelativePath = testsRelativePath; + } + + public void summarize() { + createHtmlDetails(); + createTxtSummary(); + } + + public void reset() { + mCrashedTestsCount = 0; + mFailedNotIgnoredTests.clear(); + mIgnoredTests.clear(); + mPassedNotIgnoredTests.clear(); + mDate = new Date(); + } + + private void createTxtSummary() { + StringBuilder txt = new StringBuilder(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + txt.append(mTestsRelativePath + "\n"); + txt.append("Date: " + dateFormat.format(mDate) + "\n"); + txt.append("Build fingerprint: " + Build.FINGERPRINT + "\n"); + txt.append("WebKit version: " + getWebKitVersionFromUserAgentString() + "\n"); + + txt.append("TOTAL: " + getTotalTestCount() + "\n"); + if (mCrashedTestsCount > 0) { + txt.append("CRASHED (total among all tests): " + mCrashedTestsCount + "\n"); + txt.append("-------------"); + } + txt.append("FAILED: " + mFailedNotIgnoredTests.size() + "\n"); + txt.append("IGNORED: " + mIgnoredTests.size() + "\n"); + txt.append("PASSED: " + mPassedNotIgnoredTests.size() + "\n"); + + FsUtils.writeDataToStorage(new File(mResultsRootDirPath, TXT_SUMMARY_RELATIVE_PATH), + txt.toString().getBytes(), false); + } + + private void createHtmlDetails() { + StringBuilder html = new StringBuilder(); + + html.append("<html><head>"); + html.append(CSS); + html.append(SCRIPT); + html.append("</head><body>"); + + createTopSummaryTable(html); + + createResultsListWithDiff(html, "Failed", mFailedNotIgnoredTests); + + createResultsListWithDiff(html, "Ignored", mIgnoredTests); + + createResultsListNoDiff(html, "Passed", mPassedNotIgnoredTests); + + html.append("</body></html>"); + + FsUtils.writeDataToStorage(new File(mResultsRootDirPath, HTML_DETAILS_RELATIVE_PATH), + html.toString().getBytes(), false); + } + + private int getTotalTestCount() { + return mFailedNotIgnoredTests.size() + + mPassedNotIgnoredTests.size() + + mIgnoredTests.size(); + } + + 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 void createTopSummaryTable(StringBuilder html) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + html.append("<h1>" + 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("<table class=\"summary\">"); + createSummaryTableRow(html, "TOTAL", getTotalTestCount()); + createSummaryTableRow(html, "CRASHED", mCrashedTestsCount); + createSummaryTableRow(html, "FAILED", mFailedNotIgnoredTests.size()); + createSummaryTableRow(html, "IGNORED", mIgnoredTests.size()); + createSummaryTableRow(html, "PASSED", mPassedNotIgnoredTests.size()); + 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 createResultsListWithDiff(StringBuilder html, String title, + List<AbstractResult> resultsList) { + String relativePath; + String id = ""; + AbstractResult.ResultCode resultCode; + + Collections.sort(resultsList); + html.append("<h2>" + title + " [" + resultsList.size() + "]</h2>"); + for (AbstractResult result : resultsList) { + relativePath = result.getRelativePath(); + resultCode = result.getResultCode(); + + html.append("<h3>"); + + if (resultCode == AbstractResult.ResultCode.PASS) { + html.append("<span class=\"sqr\">■ </span>"); + html.append("<span class=\"path\">" + relativePath + "</span>"); + } else { + /** + * 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, ":"); + 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>"); + } + + 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>"); + } + } + + html.append("</h3>"); + + if (resultCode != AbstractResult.ResultCode.PASS) { + 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("</div>"); + } + + html.append("<div class=\"space\"></div>"); + } + } + + private void createResultsListNoDiff(StringBuilder html, String title, + List<String> resultsList) { + Collections.sort(resultsList); + html.append("<h2>Passed [" + resultsList.size() + "]</h2>"); + for (String result : resultsList) { + html.append("<h3>"); + html.append("<span class=\"sqr\">■ </span>"); + html.append("<span class=\"path\">" + result + "</span>"); + html.append("</h3>"); + html.append("<div class=\"space\"></div>"); + } + } +}
\ 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..4965fd9 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListActivity.java @@ -0,0 +1,173 @@ +/* + * 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.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.Window; + +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) { + /** TODO: Show some kind of summary to the user */ + mEverythingFinished = true; + if (mOnEverythingFinishedCallback != null) { + mOnEverythingFinishedCallback.onFinished(); + } + } + + @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..2145af7 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TestsListPreloaderThread.java @@ -0,0 +1,121 @@ +/* + * 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 android.util.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedList; + +/** + * 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"; + + /** TODO: make it a setting */ + private static final String TESTS_ROOT_DIR_PATH = + Environment.getExternalStorageDirectory() + + File.separator + "android" + + File.separator + "LayoutTests"; + + /** A list containing relative paths of tests to run */ + private ArrayList<String> mTestsList = new ArrayList<String>(); + + private FileFilter mFileFilter; + + /** + * A relative path to the folder 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(TESTS_ROOT_DIR_PATH); + mRelativePath = path; + mDoneMsg = doneMsg; + } + + @Override + public void run() { + /** Check if the path is correct */ + File file = new File(TESTS_ROOT_DIR_PATH, mRelativePath); + if (!file.exists()) { + Log.e(LOG_TAG + "::run", "Path does not exist: " + mRelativePath); + } else { + /** Populate the tests' list accordingly */ + if (file.isDirectory()) { + preloadTests(mRelativePath); + } else { + mTestsList.add(mRelativePath); + } + } + + mDoneMsg.obj = mTestsList; + mDoneMsg.sendToTarget(); + } + + /** + * Loads all the tests from the given folders and all the subfolders + * into mTestsList. + * + * @param dirRelativePath + */ + private void preloadTests(String dirRelativePath) { + LinkedList<String> foldersList = new LinkedList<String>(); + foldersList.add(dirRelativePath); + + String relativePath; + String currentDirRelativePath; + String itemName; + File[] items; + while (!foldersList.isEmpty()) { + currentDirRelativePath = foldersList.removeFirst(); + items = new File(TESTS_ROOT_DIR_PATH, currentDirRelativePath).listFiles(); + for (File item : items) { + itemName = item.getName(); + relativePath = currentDirRelativePath + File.separator + itemName; + + if (item.isDirectory() && FileFilter.isTestDir(itemName)) { + foldersList.add(relativePath); + continue; + } + + if (FileFilter.isTestFile(itemName)) { + if (!mFileFilter.isSkip(relativePath)) { + mTestsList.add(relativePath); + } else { + //mSummarizer.addSkippedTest(relativePath); + /** TODO: Summarizer is now in service - figure out how to send the info */ + } + } + } + } + } +}
\ No newline at end of file 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..0f864e5 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/TextResult.java @@ -0,0 +1,229 @@ +/* + * 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 mActualResult; + private String mRelativePath; + private ResultCode mResultCode; + private Message mResultObtainedMsg; + + private boolean mDumpChildFramesAsText; + + 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"); + mActualResult = bundle.getString("actualTextualResult"); + setAdditionalTextOutputString(bundle.getString("additionalTextOutputString")); + mRelativePath = bundle.getString("relativePath"); + String resultCode = bundle.getString("resultCode"); + if (resultCode != null) { + mResultCode = ResultCode.valueOf(resultCode); + } + } + + @Override + public ResultCode getResultCode() { + if (mResultCode != null) { + return mResultCode; + } + + if (mExpectedResult == null) { + mResultCode = AbstractResult.ResultCode.FAIL_NO_EXPECTED_RESULT; + } else if (!mExpectedResult.equals(mActualResult)) { + mResultCode = AbstractResult.ResultCode.FAIL_RESULT_DIFFERS; + } else { + mResultCode = AbstractResult.ResultCode.PASS; + } + + return mResultCode; + } + + @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 setExpectedTextResult(String expectedResult) { + mExpectedResult = expectedResult; + } + + @Override + public String getDiffAsHtml() { + 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>"); + + if (mExpectedResult == null || mActualResult == null) { + appendNullsHtml(html); + } else { + 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); + + html.append(VisualDiffUtils.getHtml(expectedLineNums, expectedLines, + actualLineNums, actualLines)); + } + + private void appendNullsHtml(StringBuilder html) { + /** TODO: Create a separate row for each line of not null result */ + html.append(" <tr class=\"results\">"); + html.append(" <td class=\"line_count\">"); + html.append(" </td>"); + html.append(" <td class=\"line\">"); + if (mExpectedResult == null) { + html.append("Expected result was NULL"); + } else { + html.append(mExpectedResult.replace("\n", "<br />")); + } + html.append(" </td>"); + html.append(" <td class=\"space\"></td>"); + html.append(" <td class=\"line_count\">"); + html.append(" </td>"); + html.append(" <td class=\"line\">"); + if (mActualResult == null) { + html.append("Actual result was NULL"); + } else { + html.append(mActualResult.replace("\n", "<br />")); + } + html.append(" </td>"); + html.append(" </tr>"); + } + + @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("actualTextualResult", getActualTextResult()); + bundle.putString("additionalTextOutputString", getAdditionalTextOutputString()); + bundle.putString("relativePath", mRelativePath); + if (mResultCode != null) { + bundle.putString("resultCode", mResultCode.name()); + } + bundle.putString("type", getType().name()); + return bundle; + } + + @Override + public String getRelativePath() { + return mRelativePath; + } +}
\ No newline at end of file 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..250b6bc --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/VisualDiffUtils.java @@ -0,0 +1,207 @@ +/* + * 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; + for (diff_match_patch.Diff diff : diffs) { + switch (diff.operation) { + case DELETE: + line = processDiff(diff, lineNums, lines, line, i, delSpan); + if (line.equals("")) { + i++; + } + break; + + case INSERT: + if (diff.text.endsWith("\n")) { + lineNums.add(DONT_PRINT_LINE_NUMBER); + lines.add(""); + } + break; + + case EQUAL: + line = processDiff(diff, lineNums, lines, line, i, eqlSpan); + if (line.equals("")) { + i++; + } + break; + } + } + + if (!line.isEmpty()) { + lines.add(line); + lineNums.add(i); + } + } + + 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; + for (diff_match_patch.Diff diff : diffs) { + switch (diff.operation) { + case INSERT: + line = processDiff(diff, lineNums, lines, line, i, insSpan); + if (line.equals("")) { + i++; + } + break; + + case DELETE: + if (diff.text.endsWith("\n")) { + lineNums.add(DONT_PRINT_LINE_NUMBER); + lines.add(""); + } + break; + + case EQUAL: + line = processDiff(diff, lineNums, lines, line, i, eqlSpan); + if (line.equals("")) { + i++; + } + break; + } + } + + if (!line.isEmpty()) { + lines.add(line); + lineNums.add(i); + } + } + + /** + * 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 + * @return + */ + public static String processDiff(diff_match_patch.Diff diff, LinkedList<Integer> lineNums, + LinkedList<String> lines, String line, int i, String begSpan) { + String endSpan = "</span>"; + String br = " "; + + if (diff.text.endsWith("\n")) { + 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/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..ddfae69 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/scriptsupport/Starter.java @@ -0,0 +1,74 @@ +/* + * 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; + +/** + * 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(); + + 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.getMessage()); + } + } + } + } +}
\ 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..af0d7d1 --- /dev/null +++ b/tests/DumpRenderTree2/src/com/android/dumprendertree2/ui/DirListActivity.java @@ -0,0 +1,404 @@ +/* + * 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.TestsListActivity; +import com.android.dumprendertree2.R; + +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.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 java.io.File; +import java.util.ArrayList; +import java.util.Collections; +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"; + private static final String ROOT_DIR_PATH = + Environment.getExternalStorageDirectory() + + File.separator + "android" + + File.separator + "LayoutTests"; + + /** 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; + + /** Initialized lazily before first sProgressDialog.show() */ + private static ProgressDialog sProgressDialog; + + private ListView mListView; + + /** This is a relative path! */ + private String mCurrentDirPath; + + /** + * TODO: This should not be a constant, but rather be configurable from somewhere. + */ + private String mRootDirPath = ROOT_DIR_PATH; + + private FileFilter mFileFilter; + + /** + * 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); + + mFileFilter = new FileFilter(ROOT_DIR_PATH); + 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 */ + Intent intent = new Intent(); + intent.setClass(DirListActivity.this, TestsListActivity.class); + intent.setAction(Intent.ACTION_RUN); + intent.putExtra(TestsListActivity.EXTRA_TEST_PATH, item.getRelativePath()); + startActivity(intent); + } + } + }); + + 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(""); + } + + @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); + /** Run the tests */ + Intent intent = new Intent(); + intent.setClass(DirListActivity.this, TestsListActivity.class); + intent.setAction(Intent.ACTION_RUN); + intent.putExtra(TestsListActivity.EXTRA_TEST_PATH, + args.getString("relativePath")); + startActivity(intent); + } + }); + + 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) { + setListAdapter(new DirListAdapter(DirListActivity.this, + (ListItem[])msg.obj)); + delayedDialogHandler.removeMessages(MSG_SHOW_PROGRESS_DIALOG); + setTitle(shortenTitle(mCurrentDirPath)); + if (sProgressDialog != null) { + sProgressDialog.dismiss(); + } + } + } + }).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) { + File dir = new File(mRootDirPath, dirPath); + + if (!dir.exists()) { + return new ListItem[0]; + } + + List<ListItem> subDirs = new ArrayList<ListItem>(); + List<ListItem> subFiles = new ArrayList<ListItem>(); + + for (File item : dir.listFiles()) { + if (item.isDirectory() && FileFilter.isTestDir(item.getName())) { + subDirs.add(new ListItem(mFileFilter.getRelativePath(item), true)); + } else if (FileFilter.isTestFile(item.getName())) { + subFiles.add(new ListItem(mFileFilter.getRelativePath(item), false)); + } + } + + Collections.sort(subDirs); + Collections.sort(subFiles); + + /** Concatenate the two lists */ + subDirs.addAll(subFiles); + + return subDirs.toArray(new ListItem[subDirs.size()]); + } +}
\ No newline at end of file |