summaryrefslogtreecommitdiff
path: root/layout/tools
diff options
context:
space:
mode:
authorwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
committerwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
commitd25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch)
tree16ec27edc7d5f83986f16236d3a36a2682a0f37e /layout/tools
parenta942906574671868daf122284a9c4689e6924f74 (diff)
downloadpalemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'layout/tools')
-rw-r--r--layout/tools/layout-debug/application.ini12
-rw-r--r--layout/tools/layout-debug/chrome.manifest2
-rw-r--r--layout/tools/layout-debug/layoutdebug-prefs.js5
-rw-r--r--layout/tools/layout-debug/moz.build11
-rw-r--r--layout/tools/layout-debug/src/Makefile.in25
-rw-r--r--layout/tools/layout-debug/src/moz.build26
-rw-r--r--layout/tools/layout-debug/src/nsDebugFactory.cpp50
-rw-r--r--layout/tools/layout-debug/src/nsILayoutDebuggingTools.idl52
-rw-r--r--layout/tools/layout-debug/src/nsILayoutRegressionTester.idl44
-rw-r--r--layout/tools/layout-debug/src/nsLayoutDebugCIID.h24
-rw-r--r--layout/tools/layout-debug/src/nsLayoutDebugCLH.cpp84
-rw-r--r--layout/tools/layout-debug/src/nsLayoutDebugCLH.h27
-rw-r--r--layout/tools/layout-debug/src/nsLayoutDebuggingTools.cpp544
-rw-r--r--layout/tools/layout-debug/src/nsLayoutDebuggingTools.h38
-rw-r--r--layout/tools/layout-debug/src/nsRegressionTester.cpp152
-rw-r--r--layout/tools/layout-debug/src/nsRegressionTester.h36
-rw-r--r--layout/tools/layout-debug/tests/moz.build7
-rw-r--r--layout/tools/layout-debug/tests/unit/test_componentsRegistered.js8
-rw-r--r--layout/tools/layout-debug/tests/unit/xpcshell.ini5
-rw-r--r--layout/tools/layout-debug/ui/content/layoutdebug-overlay.xul38
-rw-r--r--layout/tools/layout-debug/ui/content/layoutdebug.js434
-rw-r--r--layout/tools/layout-debug/ui/content/layoutdebug.xul187
-rw-r--r--layout/tools/layout-debug/ui/jar.mn14
-rw-r--r--layout/tools/layout-debug/ui/locale/en-US/layoutdebug-overlay.dtd8
-rw-r--r--layout/tools/layout-debug/ui/locale/en-US/layoutdebug.dtd76
-rw-r--r--layout/tools/layout-debug/ui/moz.build6
-rw-r--r--layout/tools/recording/Makefile.in19
-rw-r--r--layout/tools/recording/jar.mn4
-rw-r--r--layout/tools/recording/moz.build7
-rw-r--r--layout/tools/recording/recording-cmdline.js74
-rw-r--r--layout/tools/recording/recording-cmdline.manifest3
-rw-r--r--layout/tools/recording/recording.js53
-rw-r--r--layout/tools/recording/recording.xul22
-rw-r--r--layout/tools/reftest/Makefile.in94
-rw-r--r--layout/tools/reftest/README.txt510
-rw-r--r--layout/tools/reftest/b2g_start_script.js47
-rw-r--r--layout/tools/reftest/bootstrap.js101
-rw-r--r--layout/tools/reftest/clean-reftest-output.pl38
-rw-r--r--layout/tools/reftest/install.rdf24
-rw-r--r--layout/tools/reftest/jar.mn14
-rw-r--r--layout/tools/reftest/mach_commands.py159
-rw-r--r--layout/tools/reftest/moz.build8
-rw-r--r--layout/tools/reftest/print-manifest-dirs.py79
-rw-r--r--layout/tools/reftest/reftest-analyzer.xhtml579
-rw-r--r--layout/tools/reftest/reftest-cmdline.js119
-rw-r--r--layout/tools/reftest/reftest-cmdline.manifest3
-rw-r--r--layout/tools/reftest/reftest-content.js896
-rw-r--r--layout/tools/reftest/reftest-to-html.pl118
-rw-r--r--layout/tools/reftest/reftest.js1957
-rw-r--r--layout/tools/reftest/reftest.xul22
-rw-r--r--layout/tools/reftest/remotereftest.py517
-rw-r--r--layout/tools/reftest/runreftest.py322
-rw-r--r--layout/tools/reftest/runreftestb2g.py584
-rw-r--r--layout/tools/tests/content_dumping.html101
-rw-r--r--layout/tools/tests/debug_utils.html111
-rw-r--r--layout/tools/tests/regression_tests.html132
-rw-r--r--layout/tools/tests/regression_tests.js553
57 files changed, 9185 insertions, 0 deletions
diff --git a/layout/tools/layout-debug/application.ini b/layout/tools/layout-debug/application.ini
new file mode 100644
index 000000000..27d37b272
--- /dev/null
+++ b/layout/tools/layout-debug/application.ini
@@ -0,0 +1,12 @@
+#filter substitution
+[App]
+Vendor=MozillaTest
+Name=LayoutDebug
+Version=0.1
+BuildID=@BUILD_ID@
+Copyright=Copyright (c) 2004 Mozilla.org
+ID={da915c15-c21a-41e2-95c3-2e0f76fd3191}
+
+[Gecko]
+MinVersion=@MOZILLA_VERSION_U@
+MaxVersion=@MOZILLA_VERSION_U@
diff --git a/layout/tools/layout-debug/chrome.manifest b/layout/tools/layout-debug/chrome.manifest
new file mode 100644
index 000000000..f7e957041
--- /dev/null
+++ b/layout/tools/layout-debug/chrome.manifest
@@ -0,0 +1,2 @@
+content layoutdebug jar:layoutdebug.jar!/content/layoutdebug/
+locale layoutdebug en-US jar:layoutdebug.jar!/locale/en-US/layoutdebug/
diff --git a/layout/tools/layout-debug/layoutdebug-prefs.js b/layout/tools/layout-debug/layoutdebug-prefs.js
new file mode 100644
index 000000000..bfa8e1990
--- /dev/null
+++ b/layout/tools/layout-debug/layoutdebug-prefs.js
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pref("toolkit.defaultChromeURI", "chrome://layoutdebug/content/layoutdebug.xul");
diff --git a/layout/tools/layout-debug/moz.build b/layout/tools/layout-debug/moz.build
new file mode 100644
index 000000000..3bcc23e38
--- /dev/null
+++ b/layout/tools/layout-debug/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['src', 'ui']
+TEST_DIRS += ['tests']
+
+MODULE = 'layout_debug'
+
diff --git a/layout/tools/layout-debug/src/Makefile.in b/layout/tools/layout-debug/src/Makefile.in
new file mode 100644
index 000000000..d7494198c
--- /dev/null
+++ b/layout/tools/layout-debug/src/Makefile.in
@@ -0,0 +1,25 @@
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXPORT_LIBRARY = 1
+IS_COMPONENT = 1
+MODULE_NAME = nsLayoutDebugModule
+LIBXUL_LIBRARY = 1
+FAIL_ON_WARNINGS = 1
+
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+
+clobber::
+ rm -f $(DIST)\lib\library
diff --git a/layout/tools/layout-debug/src/moz.build b/layout/tools/layout-debug/src/moz.build
new file mode 100644
index 000000000..fd03b5818
--- /dev/null
+++ b/layout/tools/layout-debug/src/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ 'nsILayoutDebuggingTools.idl',
+ 'nsILayoutRegressionTester.idl',
+]
+
+MODULE = 'layout_debug'
+
+EXPORTS += [
+ 'nsLayoutDebugCIID.h',
+]
+
+CPP_SOURCES += [
+ 'nsDebugFactory.cpp',
+ 'nsLayoutDebugCLH.cpp',
+ 'nsLayoutDebuggingTools.cpp',
+ 'nsRegressionTester.cpp',
+]
+
+LIBRARY_NAME = 'gkdebug'
+
diff --git a/layout/tools/layout-debug/src/nsDebugFactory.cpp b/layout/tools/layout-debug/src/nsDebugFactory.cpp
new file mode 100644
index 000000000..ecb7e894f
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsDebugFactory.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nscore.h"
+#include "nsLayoutDebugCIID.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsIFactory.h"
+#include "nsISupports.h"
+#include "nsRegressionTester.h"
+#include "nsLayoutDebuggingTools.h"
+#include "nsLayoutDebugCLH.h"
+#include "nsIServiceManager.h"
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsRegressionTester)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsLayoutDebuggingTools)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsLayoutDebugCLH)
+
+NS_DEFINE_NAMED_CID(NS_REGRESSION_TESTER_CID);
+NS_DEFINE_NAMED_CID(NS_LAYOUT_DEBUGGINGTOOLS_CID);
+NS_DEFINE_NAMED_CID(NS_LAYOUTDEBUGCLH_CID);
+
+static const mozilla::Module::CIDEntry kLayoutDebugCIDs[] = {
+ { &kNS_REGRESSION_TESTER_CID, false, NULL, nsRegressionTesterConstructor },
+ { &kNS_LAYOUT_DEBUGGINGTOOLS_CID, false, NULL, nsLayoutDebuggingToolsConstructor },
+ { &kNS_LAYOUTDEBUGCLH_CID, false, NULL, nsLayoutDebugCLHConstructor },
+ { NULL }
+};
+
+static const mozilla::Module::ContractIDEntry kLayoutDebugContracts[] = {
+ { "@mozilla.org/layout-debug/regressiontester;1", &kNS_REGRESSION_TESTER_CID },
+ { NS_LAYOUT_DEBUGGINGTOOLS_CONTRACTID, &kNS_LAYOUT_DEBUGGINGTOOLS_CID },
+ { "@mozilla.org/commandlinehandler/general-startup;1?type=layoutdebug", &kNS_LAYOUTDEBUGCLH_CID },
+ { NULL }
+};
+
+static const mozilla::Module::CategoryEntry kLayoutDebugCategories[] = {
+ { "command-line-handler", "m-layoutdebug", "@mozilla.org/commandlinehandler/general-startup;1?type=layoutdebug" },
+ { NULL }
+};
+
+static const mozilla::Module kLayoutDebugModule = {
+ mozilla::Module::kVersion,
+ kLayoutDebugCIDs,
+ kLayoutDebugContracts,
+ kLayoutDebugCategories
+};
+
+NSMODULE_DEFN(nsLayoutDebugModule) = &kLayoutDebugModule;
diff --git a/layout/tools/layout-debug/src/nsILayoutDebuggingTools.idl b/layout/tools/layout-debug/src/nsILayoutDebuggingTools.idl
new file mode 100644
index 000000000..ae0bd9d13
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsILayoutDebuggingTools.idl
@@ -0,0 +1,52 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+// vim:cindent:tabstop=4:expandtab:shiftwidth=4:
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+
+/**
+ * A series of hooks into non-IDL-ized layout code to allow all the
+ * layout debugging functions to be used from chrome.
+ */
+
+[scriptable, uuid(4b968d4b-9c08-4635-a7e0-55084843f0fd)]
+interface nsILayoutDebuggingTools : nsISupports
+{
+
+ /*
+ * Initialize debugger object to act on a docshell.
+ */
+ void init(in nsIDOMWindow win);
+
+ /*
+ * Notify the debugger that the docshell has been told to load a new
+ * URI.
+ */
+ void newURILoaded();
+
+ /* Toggle various debugging states */
+ attribute boolean visualDebugging;
+ attribute boolean visualEventDebugging;
+ attribute boolean paintFlashing;
+ attribute boolean paintDumping;
+ attribute boolean invalidateDumping;
+ attribute boolean eventDumping;
+ attribute boolean motionEventDumping;
+ attribute boolean crossingEventDumping;
+ attribute boolean reflowCounts;
+
+ /* Run various tests. */
+ void dumpWebShells();
+ void dumpContent();
+ void dumpFrames();
+ void dumpViews();
+
+ void dumpStyleSheets();
+ void dumpStyleContexts();
+
+ void dumpReflowStats();
+};
diff --git a/layout/tools/layout-debug/src/nsILayoutRegressionTester.idl b/layout/tools/layout-debug/src/nsILayoutRegressionTester.idl
new file mode 100644
index 000000000..9407ecdf1
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsILayoutRegressionTester.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+interface nsIFile;
+
+[scriptable, uuid(B249B2C0-EE11-11DA-8AD9-0800200C9A66)]
+interface nsILayoutRegressionTester : nsISupports
+{
+ /**
+ * Dumps the content of a window
+ * @param aWindowToDump the window to dump (may be an iframe etc)
+ * @param aFile the file to dump to. It will be created if necessary, otherwise
+ truncated. If nil, write to stdout.
+ * @param aFlagsMask some flags that determine what to dump
+ * @param aFlagsMask some flags that determine what to dump
+ * @param aResult a status value indicating whether the dump happened,
+ whether the page was still loading, or whether some other error happened.
+ */
+ const short DUMP_FLAGS_MASK_DEFAULT = 0;
+ const short DUMP_FLAGS_MASK_PRINT_MODE = 1;
+
+ const long DUMP_RESULT_COMPLETED = 0; // loaded OK
+ const long DUMP_RESULT_LOADING = 1; // still loading
+ const long DUMP_RESULT_ERROR = 2; // an error occurred
+
+ long dumpFrameModel(in nsIDOMWindow aWindowToDump, in nsIFile aFile, in unsigned long aFlagsMask);
+
+ /**
+ * Compares the contents of frame model files
+ * @param aBaseFile the baseline file, opened with read permissions
+ * @param aVerFile file containing the results to verify, opened with read permissions
+ * @param aFlags flags specifying output verbosity
+ * @param aResult result of the comparison: zero if the files are same, non-zero if different
+ */
+ const short COMPARE_FLAGS_VERBOSE = 0;
+ const short COMPARE_FLAGS_BRIEF = 1;
+ boolean compareFrameModels(in nsIFile aBaseFile, in nsIFile aVerFile, in unsigned long aFlags);
+};
+
diff --git a/layout/tools/layout-debug/src/nsLayoutDebugCIID.h b/layout/tools/layout-debug/src/nsLayoutDebugCIID.h
new file mode 100644
index 000000000..ceff68355
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsLayoutDebugCIID.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsFrameDebugCIID_h__
+#define nsFrameDebugCIID_h__
+
+#include "nsISupports.h"
+#include "nsIFactory.h"
+#include "nsIComponentManager.h"
+
+#define NS_REGRESSION_TESTER_CID \
+{ 0x698c54f4, 0x4ea9, 0x11d7, \
+{ 0x85, 0x9f, 0x00, 0x03, 0x93, 0x63, 0x65, 0x92 } }
+
+#define NS_LAYOUT_DEBUGGINGTOOLS_CONTRACTID \
+ "@mozilla.org/layout-debug/layout-debuggingtools;1"
+// 3f4c3b63-e640-4712-abbf-fff1301ceb60
+#define NS_LAYOUT_DEBUGGINGTOOLS_CID { 0x3f4c3b68, 0xe640, 0x4712, \
+ { 0xab, 0xbf, 0xff, 0xf1, 0x30, 0x1c, 0xeb, 0x60}}
+
+#endif // nsFrameDebugCIID_h__
+
diff --git a/layout/tools/layout-debug/src/nsLayoutDebugCLH.cpp b/layout/tools/layout-debug/src/nsLayoutDebugCLH.cpp
new file mode 100644
index 000000000..74d3b090f
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsLayoutDebugCLH.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+// vim:cindent:tabstop=4:expandtab:shiftwidth=4:
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsLayoutDebugCLH.h"
+#include "nsString.h"
+#include "plstr.h"
+#include "nsCOMPtr.h"
+#include "nsIWindowWatcher.h"
+#include "nsIServiceManager.h"
+#include "nsIDOMWindow.h"
+#include "nsISupportsArray.h"
+#include "nsISupportsPrimitives.h"
+#include "nsICommandLine.h"
+
+nsLayoutDebugCLH::nsLayoutDebugCLH()
+{
+}
+
+nsLayoutDebugCLH::~nsLayoutDebugCLH()
+{
+}
+
+NS_IMPL_ISUPPORTS1(nsLayoutDebugCLH, ICOMMANDLINEHANDLER)
+
+NS_IMETHODIMP
+nsLayoutDebugCLH::Handle(nsICommandLine* aCmdLine)
+{
+ nsresult rv;
+
+ int32_t idx;
+ rv = aCmdLine->FindFlag(NS_LITERAL_STRING("layoutdebug"), false, &idx);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (idx < 0)
+ return NS_OK;
+
+ int32_t length;
+ aCmdLine->GetLength(&length);
+
+ nsAutoString url;
+ if (idx + 1 < length) {
+ rv = aCmdLine->GetArgument(idx + 1, url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!url.IsEmpty() && url.CharAt(0) == '-')
+ url.Truncate();
+ }
+
+ aCmdLine->RemoveArguments(idx, idx + !url.IsEmpty());
+
+ nsCOMPtr<nsISupportsArray> argsArray =
+ do_CreateInstance(NS_SUPPORTSARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!url.IsEmpty())
+ {
+ nsCOMPtr<nsISupportsString> scriptableURL =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ NS_ENSURE_TRUE(scriptableURL, NS_ERROR_FAILURE);
+
+ scriptableURL->SetData(url);
+ argsArray->AppendElement(scriptableURL);
+ }
+
+ nsCOMPtr<nsIWindowWatcher> wwatch =
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID);
+ NS_ENSURE_TRUE(wwatch, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMWindow> opened;
+ wwatch->OpenWindow(nullptr, "chrome://layoutdebug/content/",
+ "_blank", "chrome,dialog=no,all", argsArray,
+ getter_AddRefs(opened));
+ aCmdLine->SetPreventDefault(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebugCLH::GetHelpInfo(nsACString& aResult)
+{
+ aResult.Assign(NS_LITERAL_CSTRING(" -layoutdebug [<url>] Start with Layout Debugger\n"));
+ return NS_OK;
+}
+
diff --git a/layout/tools/layout-debug/src/nsLayoutDebugCLH.h b/layout/tools/layout-debug/src/nsLayoutDebugCLH.h
new file mode 100644
index 000000000..c2d5108c2
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsLayoutDebugCLH.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+// vim:cindent:tabstop=4:expandtab:shiftwidth=4:
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsLayoutDebugCLH_h_
+#define nsLayoutDebugCLH_h_
+
+#include "nsICommandLineHandler.h"
+#define ICOMMANDLINEHANDLER nsICommandLineHandler
+
+#define NS_LAYOUTDEBUGCLH_CID \
+ { 0xa8f52633, 0x5ecf, 0x424a, \
+ { 0xa1, 0x47, 0x47, 0xc3, 0x22, 0xf7, 0xbc, 0xe2 }}
+
+class nsLayoutDebugCLH : public ICOMMANDLINEHANDLER
+{
+public:
+ nsLayoutDebugCLH();
+ virtual ~nsLayoutDebugCLH();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOMMANDLINEHANDLER
+};
+
+#endif /* !defined(nsLayoutDebugCLH_h_) */
diff --git a/layout/tools/layout-debug/src/nsLayoutDebuggingTools.cpp b/layout/tools/layout-debug/src/nsLayoutDebuggingTools.cpp
new file mode 100644
index 000000000..b9d4f6d09
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsLayoutDebuggingTools.cpp
@@ -0,0 +1,544 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+// vim:cindent:tabstop=4:expandtab:shiftwidth=4:
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsLayoutDebuggingTools.h"
+
+#include "nsIDocShell.h"
+#include "nsPIDOMWindow.h"
+#include "nsIContentViewer.h"
+
+#include "nsIServiceManager.h"
+#include "nsIAtom.h"
+#include "nsQuickSort.h"
+
+#include "nsIContent.h"
+#include "nsIDocument.h"
+#include "nsIDOMDocument.h"
+
+#include "nsIPresShell.h"
+#include "nsViewManager.h"
+#include "nsIFrame.h"
+
+#include "nsILayoutDebugger.h"
+#include "nsLayoutCID.h"
+static NS_DEFINE_CID(kLayoutDebuggerCID, NS_LAYOUT_DEBUGGER_CID);
+
+#include "nsISelectionController.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Preferences.h"
+
+using namespace mozilla;
+
+static already_AddRefed<nsIContentViewer>
+doc_viewer(nsIDocShell *aDocShell)
+{
+ if (!aDocShell)
+ return nullptr;
+ nsCOMPtr<nsIContentViewer> result;
+ aDocShell->GetContentViewer(getter_AddRefs(result));
+ return result.forget();
+}
+
+static already_AddRefed<nsIPresShell>
+pres_shell(nsIDocShell *aDocShell)
+{
+ nsCOMPtr<nsIContentViewer> cv = doc_viewer(aDocShell);
+ if (!cv)
+ return nullptr;
+ nsCOMPtr<nsIPresShell> result;
+ cv->GetPresShell(getter_AddRefs(result));
+ return result.forget();
+}
+
+static nsViewManager*
+view_manager(nsIDocShell *aDocShell)
+{
+ nsCOMPtr<nsIPresShell> shell(pres_shell(aDocShell));
+ if (!shell)
+ return nullptr;
+ return shell->GetViewManager();
+}
+
+#ifdef DEBUG
+static already_AddRefed<nsIDocument>
+document(nsIDocShell *aDocShell)
+{
+ nsCOMPtr<nsIContentViewer> cv(doc_viewer(aDocShell));
+ if (!cv)
+ return nullptr;
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ cv->GetDOMDocument(getter_AddRefs(domDoc));
+ if (!domDoc)
+ return nullptr;
+ nsCOMPtr<nsIDocument> result = do_QueryInterface(domDoc);
+ return result.forget();
+}
+#endif
+
+nsLayoutDebuggingTools::nsLayoutDebuggingTools()
+ : mPaintFlashing(false),
+ mPaintDumping(false),
+ mInvalidateDumping(false),
+ mEventDumping(false),
+ mMotionEventDumping(false),
+ mCrossingEventDumping(false),
+ mReflowCounts(false)
+{
+ NewURILoaded();
+}
+
+nsLayoutDebuggingTools::~nsLayoutDebuggingTools()
+{
+}
+
+NS_IMPL_ISUPPORTS1(nsLayoutDebuggingTools, nsILayoutDebuggingTools)
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::Init(nsIDOMWindow *aWin)
+{
+ if (!Preferences::GetService()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ {
+ nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aWin);
+ if (!window)
+ return NS_ERROR_UNEXPECTED;
+ mDocShell = window->GetDocShell();
+ }
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_UNEXPECTED);
+
+ mPaintFlashing =
+ Preferences::GetBool("nglayout.debug.paint_flashing", mPaintFlashing);
+ mPaintDumping =
+ Preferences::GetBool("nglayout.debug.paint_dumping", mPaintDumping);
+ mInvalidateDumping =
+ Preferences::GetBool("nglayout.debug.invalidate_dumping", mInvalidateDumping);
+ mEventDumping =
+ Preferences::GetBool("nglayout.debug.event_dumping", mEventDumping);
+ mMotionEventDumping =
+ Preferences::GetBool("nglayout.debug.motion_event_dumping",
+ mMotionEventDumping);
+ mCrossingEventDumping =
+ Preferences::GetBool("nglayout.debug.crossing_event_dumping",
+ mCrossingEventDumping);
+ mReflowCounts =
+ Preferences::GetBool("layout.reflow.showframecounts", mReflowCounts);
+
+ {
+ nsCOMPtr<nsILayoutDebugger> ld = do_GetService(kLayoutDebuggerCID);
+ if (ld) {
+ ld->GetShowFrameBorders(&mVisualDebugging);
+ ld->GetShowEventTargetFrameBorder(&mVisualEventDebugging);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::NewURILoaded()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+ // Reset all the state that should be reset between pages.
+
+ // XXX Some of these should instead be transferred between pages!
+ mEditorMode = false;
+ mVisualDebugging = false;
+ mVisualEventDebugging = false;
+
+ mReflowCounts = false;
+
+ ForceRefresh();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetVisualDebugging(bool *aVisualDebugging)
+{
+ *aVisualDebugging = mVisualDebugging;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetVisualDebugging(bool aVisualDebugging)
+{
+ nsCOMPtr<nsILayoutDebugger> ld = do_GetService(kLayoutDebuggerCID);
+ if (!ld)
+ return NS_ERROR_UNEXPECTED;
+ mVisualDebugging = aVisualDebugging;
+ ld->SetShowFrameBorders(aVisualDebugging);
+ ForceRefresh();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetVisualEventDebugging(bool *aVisualEventDebugging)
+{
+ *aVisualEventDebugging = mVisualEventDebugging;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetVisualEventDebugging(bool aVisualEventDebugging)
+{
+ nsCOMPtr<nsILayoutDebugger> ld = do_GetService(kLayoutDebuggerCID);
+ if (!ld)
+ return NS_ERROR_UNEXPECTED;
+ mVisualEventDebugging = aVisualEventDebugging;
+ ld->SetShowEventTargetFrameBorder(aVisualEventDebugging);
+ ForceRefresh();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetPaintFlashing(bool *aPaintFlashing)
+{
+ *aPaintFlashing = mPaintFlashing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetPaintFlashing(bool aPaintFlashing)
+{
+ mPaintFlashing = aPaintFlashing;
+ return SetBoolPrefAndRefresh("nglayout.debug.paint_flashing", mPaintFlashing);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetPaintDumping(bool *aPaintDumping)
+{
+ *aPaintDumping = mPaintDumping;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetPaintDumping(bool aPaintDumping)
+{
+ mPaintDumping = aPaintDumping;
+ return SetBoolPrefAndRefresh("nglayout.debug.paint_dumping", mPaintDumping);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetInvalidateDumping(bool *aInvalidateDumping)
+{
+ *aInvalidateDumping = mInvalidateDumping;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetInvalidateDumping(bool aInvalidateDumping)
+{
+ mInvalidateDumping = aInvalidateDumping;
+ return SetBoolPrefAndRefresh("nglayout.debug.invalidate_dumping", mInvalidateDumping);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetEventDumping(bool *aEventDumping)
+{
+ *aEventDumping = mEventDumping;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetEventDumping(bool aEventDumping)
+{
+ mEventDumping = aEventDumping;
+ return SetBoolPrefAndRefresh("nglayout.debug.event_dumping", mEventDumping);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetMotionEventDumping(bool *aMotionEventDumping)
+{
+ *aMotionEventDumping = mMotionEventDumping;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetMotionEventDumping(bool aMotionEventDumping)
+{
+ mMotionEventDumping = aMotionEventDumping;
+ return SetBoolPrefAndRefresh("nglayout.debug.motion_event_dumping", mMotionEventDumping);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetCrossingEventDumping(bool *aCrossingEventDumping)
+{
+ *aCrossingEventDumping = mCrossingEventDumping;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetCrossingEventDumping(bool aCrossingEventDumping)
+{
+ mCrossingEventDumping = aCrossingEventDumping;
+ return SetBoolPrefAndRefresh("nglayout.debug.crossing_event_dumping", mCrossingEventDumping);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::GetReflowCounts(bool* aShow)
+{
+ *aShow = mReflowCounts;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::SetReflowCounts(bool aShow)
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPresShell> shell(pres_shell(mDocShell));
+ if (shell) {
+#ifdef MOZ_REFLOW_PERF
+ shell->SetPaintFrameCount(aShow);
+ SetBoolPrefAndRefresh("layout.reflow.showframecounts", aShow);
+ mReflowCounts = aShow;
+#else
+ printf("************************************************\n");
+ printf("Sorry, you have not built with MOZ_REFLOW_PERF=1\n");
+ printf("************************************************\n");
+#endif
+ }
+ return NS_OK;
+}
+
+static void DumpAWebShell(nsIDocShellTreeItem* aShellItem, FILE* out, int32_t aIndent)
+{
+ nsString name;
+ nsCOMPtr<nsIDocShellTreeItem> parent;
+ int32_t i, n;
+
+ for (i = aIndent; --i >= 0; )
+ fprintf(out, " ");
+
+ fprintf(out, "%p '", static_cast<void*>(aShellItem));
+ aShellItem->GetName(name);
+ aShellItem->GetSameTypeParent(getter_AddRefs(parent));
+ fputs(NS_LossyConvertUTF16toASCII(name).get(), out);
+ fprintf(out, "' parent=%p <\n", static_cast<void*>(parent));
+
+ ++aIndent;
+ nsCOMPtr<nsIDocShellTreeNode> shellAsNode(do_QueryInterface(aShellItem));
+ shellAsNode->GetChildCount(&n);
+ for (i = 0; i < n; ++i) {
+ nsCOMPtr<nsIDocShellTreeItem> child;
+ shellAsNode->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ DumpAWebShell(child, out, aIndent);
+ }
+ }
+ --aIndent;
+ for (i = aIndent; --i >= 0; )
+ fprintf(out, " ");
+ fputs(">\n", out);
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpWebShells()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+ DumpAWebShell(mDocShell, stdout, 0);
+ return NS_OK;
+}
+
+static
+void
+DumpContentRecur(nsIDocShell* aDocShell, FILE* out)
+{
+#ifdef DEBUG
+ if (nullptr != aDocShell) {
+ fprintf(out, "docshell=%p \n", static_cast<void*>(aDocShell));
+ nsCOMPtr<nsIDocument> doc(document(aDocShell));
+ if (doc) {
+ dom::Element *root = doc->GetRootElement();
+ if (root) {
+ root->List(out);
+ }
+ }
+ else {
+ fputs("no document\n", out);
+ }
+ // dump the frames of the sub documents
+ int32_t i, n;
+ aDocShell->GetChildCount(&n);
+ for (i = 0; i < n; ++i) {
+ nsCOMPtr<nsIDocShellTreeItem> child;
+ aDocShell->GetChildAt(i, getter_AddRefs(child));
+ nsCOMPtr<nsIDocShell> childAsShell(do_QueryInterface(child));
+ if (child) {
+ DumpContentRecur(childAsShell, out);
+ }
+ }
+ }
+#endif
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpContent()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+ DumpContentRecur(mDocShell, stdout);
+ return NS_OK;
+}
+
+static void
+DumpFramesRecur(nsIDocShell* aDocShell, FILE* out)
+{
+#ifdef DEBUG
+ fprintf(out, "webshell=%p \n", static_cast<void*>(aDocShell));
+ nsCOMPtr<nsIPresShell> shell(pres_shell(aDocShell));
+ if (shell) {
+ nsIFrame* root = shell->GetRootFrame();
+ if (root) {
+ root->List(out, 0);
+ }
+ }
+ else {
+ fputs("null pres shell\n", out);
+ }
+
+ // dump the frames of the sub documents
+ int32_t i, n;
+ aDocShell->GetChildCount(&n);
+ for (i = 0; i < n; ++i) {
+ nsCOMPtr<nsIDocShellTreeItem> child;
+ aDocShell->GetChildAt(i, getter_AddRefs(child));
+ nsCOMPtr<nsIDocShell> childAsShell(do_QueryInterface(child));
+ if (childAsShell) {
+ DumpFramesRecur(childAsShell, out);
+ }
+ }
+#endif
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpFrames()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+ DumpFramesRecur(mDocShell, stdout);
+ return NS_OK;
+}
+
+static
+void
+DumpViewsRecur(nsIDocShell* aDocShell, FILE* out)
+{
+#ifdef DEBUG
+ fprintf(out, "docshell=%p \n", static_cast<void*>(aDocShell));
+ nsRefPtr<nsViewManager> vm(view_manager(aDocShell));
+ if (vm) {
+ nsView* root = vm->GetRootView();
+ if (root) {
+ root->List(out);
+ }
+ }
+ else {
+ fputs("null view manager\n", out);
+ }
+
+ // dump the views of the sub documents
+ int32_t i, n;
+ aDocShell->GetChildCount(&n);
+ for (i = 0; i < n; i++) {
+ nsCOMPtr<nsIDocShellTreeItem> child;
+ aDocShell->GetChildAt(i, getter_AddRefs(child));
+ nsCOMPtr<nsIDocShell> childAsShell(do_QueryInterface(child));
+ if (childAsShell) {
+ DumpViewsRecur(childAsShell, out);
+ }
+ }
+#endif // DEBUG
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpViews()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+ DumpViewsRecur(mDocShell, stdout);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpStyleSheets()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+#ifdef DEBUG
+ FILE *out = stdout;
+ nsCOMPtr<nsIPresShell> shell(pres_shell(mDocShell));
+ if (shell)
+ shell->ListStyleSheets(out);
+ else
+ fputs("null pres shell\n", out);
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpStyleContexts()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+#ifdef DEBUG
+ FILE *out = stdout;
+ nsCOMPtr<nsIPresShell> shell(pres_shell(mDocShell));
+ if (shell) {
+ nsIFrame* root = shell->GetRootFrame();
+ if (!root) {
+ fputs("null root frame\n", out);
+ } else {
+ shell->ListStyleContexts(root, out);
+ }
+ } else {
+ fputs("null pres shell\n", out);
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLayoutDebuggingTools::DumpReflowStats()
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+#ifdef DEBUG
+ nsCOMPtr<nsIPresShell> shell(pres_shell(mDocShell));
+ if (shell) {
+#ifdef MOZ_REFLOW_PERF
+ shell->DumpReflows();
+#else
+ printf("************************************************\n");
+ printf("Sorry, you have not built with MOZ_REFLOW_PERF=1\n");
+ printf("************************************************\n");
+#endif
+ }
+#endif
+ return NS_OK;
+}
+
+void nsLayoutDebuggingTools::ForceRefresh()
+{
+ nsRefPtr<nsViewManager> vm(view_manager(mDocShell));
+ if (!vm)
+ return;
+ nsView* root = vm->GetRootView();
+ if (root) {
+ vm->InvalidateView(root);
+ }
+}
+
+nsresult
+nsLayoutDebuggingTools::SetBoolPrefAndRefresh(const char * aPrefName,
+ bool aNewVal)
+{
+ NS_ENSURE_TRUE(mDocShell, NS_ERROR_NOT_INITIALIZED);
+
+ nsIPrefService* prefService = Preferences::GetService();
+ NS_ENSURE_TRUE(prefService && aPrefName, NS_OK);
+
+ Preferences::SetBool(aPrefName, aNewVal);
+ prefService->SavePrefFile(nullptr);
+
+ ForceRefresh();
+
+ return NS_OK;
+}
diff --git a/layout/tools/layout-debug/src/nsLayoutDebuggingTools.h b/layout/tools/layout-debug/src/nsLayoutDebuggingTools.h
new file mode 100644
index 000000000..4826cebe5
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsLayoutDebuggingTools.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+// vim:cindent:tabstop=4:expandtab:shiftwidth=4:
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsILayoutDebuggingTools.h"
+#include "nsIDocShell.h"
+#include "nsCOMPtr.h"
+
+class nsLayoutDebuggingTools : public nsILayoutDebuggingTools {
+
+public:
+ nsLayoutDebuggingTools();
+ virtual ~nsLayoutDebuggingTools();
+
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSILAYOUTDEBUGGINGTOOLS
+
+protected:
+ void ForceRefresh();
+ nsresult GetBoolPref(const char * aPrefName, bool *aValue);
+ nsresult SetBoolPrefAndRefresh(const char * aPrefName, bool aNewValue);
+
+ nsCOMPtr<nsIDocShell> mDocShell;
+
+ bool mEditorMode;
+ bool mVisualDebugging;
+ bool mVisualEventDebugging;
+ bool mPaintFlashing;
+ bool mPaintDumping;
+ bool mInvalidateDumping;
+ bool mEventDumping;
+ bool mMotionEventDumping;
+ bool mCrossingEventDumping;
+ bool mReflowCounts;
+};
diff --git a/layout/tools/layout-debug/src/nsRegressionTester.cpp b/layout/tools/layout-debug/src/nsRegressionTester.cpp
new file mode 100644
index 000000000..652caf1ee
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsRegressionTester.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.h"
+#include "nsRegressionTester.h"
+
+#include "nsXPIDLString.h"
+#include "nsReadableUtils.h"
+#include "nsIWindowWatcher.h"
+#include "nsVoidArray.h"
+#include "nsPIDOMWindow.h"
+#include "nsIPresShell.h"
+#include "nsIURI.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIDocShell.h"
+#include "nsIContentViewer.h"
+#include "nsIContentViewerFile.h"
+#include "nsIFrame.h"
+#include "nsStyleStruct.h"
+#include "nsIFrameUtil.h"
+#include "nsLayoutCID.h"
+#include "nsNetUtil.h"
+#include "nsIFile.h"
+#include "nsViewManager.h"
+#include "nsView.h"
+
+
+
+static NS_DEFINE_CID(kFrameUtilCID, NS_FRAME_UTIL_CID);
+
+
+nsRegressionTester::nsRegressionTester()
+{
+}
+
+nsRegressionTester::~nsRegressionTester()
+{
+}
+
+NS_IMPL_ISUPPORTS1(nsRegressionTester, nsILayoutRegressionTester)
+
+NS_IMETHODIMP
+nsRegressionTester::DumpFrameModel(nsIDOMWindow *aWindowToDump,
+ nsIFile *aDestFile,
+ uint32_t aFlagsMask, int32_t *aResult)
+{
+ NS_ENSURE_ARG(aWindowToDump);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = DUMP_RESULT_ERROR;
+
+#ifndef DEBUG
+ return NS_ERROR_NOT_AVAILABLE;
+#else
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+ uint32_t busyFlags;
+ bool stillLoading;
+
+ nsCOMPtr<nsIDocShell> docShell;
+ rv = GetDocShellFromWindow(aWindowToDump, getter_AddRefs(docShell));
+ if (NS_FAILED(rv)) return rv;
+
+ // find out if the document is loaded
+ docShell->GetBusyFlags(&busyFlags);
+ stillLoading = busyFlags & (nsIDocShell::BUSY_FLAGS_BUSY |
+ nsIDocShell::BUSY_FLAGS_PAGE_LOADING);
+ if (stillLoading)
+ {
+ *aResult = DUMP_RESULT_LOADING;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
+
+ nsIFrame* root = presShell->GetRootFrame();
+
+ FILE* fp = stdout;
+ if (aDestFile)
+ {
+ rv = aDestFile->OpenANSIFileDesc("w", &fp);
+ if (NS_FAILED(rv)) return rv;
+ }
+ if (aFlagsMask & DUMP_FLAGS_MASK_PRINT_MODE) {
+ nsCOMPtr <nsIContentViewer> viewer;
+ docShell->GetContentViewer(getter_AddRefs(viewer));
+ if (viewer){
+ nsCOMPtr<nsIContentViewerFile> viewerFile = do_QueryInterface(viewer);
+ if (viewerFile) {
+ viewerFile->Print(true, fp, nullptr);
+ }
+ }
+ }
+ else {
+ root->DumpRegressionData(presShell->GetPresContext(), fp, 0);
+ }
+ if (fp != stdout)
+ fclose(fp);
+ *aResult = DUMP_RESULT_COMPLETED;
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+nsRegressionTester::CompareFrameModels(nsIFile *aBaseFile, nsIFile *aVerFile,
+ uint32_t aFlags, bool *aResult)
+{
+ NS_ENSURE_ARG(aBaseFile);
+ NS_ENSURE_ARG(aVerFile);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = false;
+
+ nsresult rv;
+ FILE* baseFile;
+ rv = aBaseFile->OpenANSIFileDesc("r", &baseFile);
+ if (NS_FAILED(rv)) return rv;
+
+ FILE* verFile;
+ rv = aVerFile->OpenANSIFileDesc("r", &verFile);
+ if (NS_FAILED(rv)) {
+ fclose(baseFile);
+ return rv;
+ }
+
+ nsCOMPtr<nsIFrameUtil> frameUtil = do_CreateInstance(kFrameUtilCID, &rv);
+ if (NS_SUCCEEDED(rv))
+ {
+ int32_t outputLevel = (aFlags == COMPARE_FLAGS_VERBOSE) ? 0 : 1;
+ rv = frameUtil->CompareRegressionData(baseFile, verFile, outputLevel);
+ // CompareRegressionData closes |baseFile| and |verFile|.
+ } else {
+ fclose(verFile);
+ fclose(baseFile);
+ }
+
+ *aResult = NS_FAILED(rv);
+ return NS_OK;
+}
+
+nsresult
+nsRegressionTester::GetDocShellFromWindow(nsIDOMWindow* inWindow, nsIDocShell** outShell)
+{
+ nsCOMPtr<nsPIDOMWindow> window(do_QueryInterface(inWindow));
+ if (!window) return NS_ERROR_FAILURE;
+
+ *outShell = window->GetDocShell();
+ NS_IF_ADDREF(*outShell);
+
+ return NS_OK;
+}
diff --git a/layout/tools/layout-debug/src/nsRegressionTester.h b/layout/tools/layout-debug/src/nsRegressionTester.h
new file mode 100644
index 000000000..9a5908e6a
--- /dev/null
+++ b/layout/tools/layout-debug/src/nsRegressionTester.h
@@ -0,0 +1,36 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsRegressionTester_h__
+#define nsRegressionTester_h__
+
+#include "nsCOMPtr.h"
+
+#include "nsILayoutRegressionTester.h"
+#include "nsILayoutDebugger.h"
+
+class nsIDOMWindow;
+class nsIDocShell;
+
+//*****************************************************************************
+//*** nsRegressionTester
+//*****************************************************************************
+class nsRegressionTester : public nsILayoutRegressionTester
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSILAYOUTREGRESSIONTESTER
+
+ nsRegressionTester();
+ virtual ~nsRegressionTester();
+
+protected:
+ nsresult GetDocShellFromWindow(nsIDOMWindow* inWindow, nsIDocShell** outShell);
+};
+
+
+
+#endif /* nsRegressionTester_h__ */
diff --git a/layout/tools/layout-debug/tests/moz.build b/layout/tools/layout-debug/tests/moz.build
new file mode 100644
index 000000000..191c90f0b
--- /dev/null
+++ b/layout/tools/layout-debug/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
diff --git a/layout/tools/layout-debug/tests/unit/test_componentsRegistered.js b/layout/tools/layout-debug/tests/unit/test_componentsRegistered.js
new file mode 100644
index 000000000..6a528b768
--- /dev/null
+++ b/layout/tools/layout-debug/tests/unit/test_componentsRegistered.js
@@ -0,0 +1,8 @@
+function run_test() {
+ do_check_true("@mozilla.org/layout-debug/regressiontester;1" in
+ Components.classes);
+ do_check_true("@mozilla.org/layout-debug/layout-debuggingtools;1" in
+ Components.classes);
+ do_check_true("@mozilla.org/commandlinehandler/general-startup;1?type=layoutdebug" in
+ Components.classes);
+} \ No newline at end of file
diff --git a/layout/tools/layout-debug/tests/unit/xpcshell.ini b/layout/tools/layout-debug/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..e0ee46a2c
--- /dev/null
+++ b/layout/tools/layout-debug/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_componentsRegistered.js]
diff --git a/layout/tools/layout-debug/ui/content/layoutdebug-overlay.xul b/layout/tools/layout-debug/ui/content/layoutdebug-overlay.xul
new file mode 100644
index 000000000..df34e0953
--- /dev/null
+++ b/layout/tools/layout-debug/ui/content/layoutdebug-overlay.xul
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+
+<!--
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!--
+ This file contains the nodes that will be overlayed on top of
+ <chrome://communicator/content/tasksOverlay.xul>.
+ Declare XML entites that this file refers to in layoutdebug-overlay.dtd.
+ -->
+
+<!DOCTYPE window SYSTEM "chrome://layoutdebug/locale/layoutdebug-overlay.dtd" >
+
+<overlay id="layoutdebugTaskMenuID"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<!-- SeaMonkey -->
+<menupopup id="taskPopup">
+ <menuitem label="&ldbCmd.label;"
+ accesskey="&ldbCmd.accesskey;"
+ oncommand="toOpenWindowByType('mozapp:layoutdebug',
+ 'chrome://layoutdebug/content/');"/>
+</menupopup>
+
+<!-- Firefox -->
+<menupopup id="menu_ToolsPopup">
+ <menuitem label="&ldbCmd.label;"
+ accesskey="&ldbCmd.accesskey;"
+ insertafter="javascriptConsole"
+ oncommand="toOpenWindowByType('mozapp:layoutdebug',
+ 'chrome://layoutdebug/content/');"/>
+</menupopup>
+
+</overlay>
diff --git a/layout/tools/layout-debug/ui/content/layoutdebug.js b/layout/tools/layout-debug/ui/content/layoutdebug.js
new file mode 100644
index 000000000..2c37a6478
--- /dev/null
+++ b/layout/tools/layout-debug/ui/content/layoutdebug.js
@@ -0,0 +1,434 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gBrowser;
+var gProgressListener;
+var gDebugger;
+var gRTestIndexList;
+var gRTestURLList = null;
+
+const nsILayoutDebuggingTools = Components.interfaces.nsILayoutDebuggingTools;
+const nsIDocShell = Components.interfaces.nsIDocShell;
+const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+
+const NS_LAYOUT_DEBUGGINGTOOLS_CONTRACTID = "@mozilla.org/layout-debug/layout-debuggingtools;1";
+
+
+function nsLDBBrowserContentListener()
+{
+ this.init();
+}
+
+nsLDBBrowserContentListener.prototype = {
+
+ init : function()
+ {
+ this.mStatusText = document.getElementById("status-text");
+ this.mURLBar = document.getElementById("urlbar");
+ this.mForwardButton = document.getElementById("forward-button");
+ this.mBackButton = document.getElementById("back-button");
+ this.mStopButton = document.getElementById("stop-button");
+ },
+
+ QueryInterface : function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ // nsIWebProgressListener implementation
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (!(aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
+ aWebProgress != gBrowser.webProgress)
+ return;
+
+ if (aStateFlags & nsIWebProgressListener.STATE_START) {
+ this.setButtonEnabled(this.mStopButton, true);
+ this.setButtonEnabled(this.mForwardButton, gBrowser.canGoForward);
+ this.setButtonEnabled(this.mBackButton, gBrowser.canGoBack);
+ this.mStatusText.value = "loading...";
+ this.mLoading = true;
+
+ } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
+ this.setButtonEnabled(this.mStopButton, false);
+ this.mStatusText.value = this.mURLBar.value + " loaded";
+
+ if (gRTestURLList && this.mLoading) {
+ // Let other things happen in the first 20ms, since this
+ // doesn't really seem to be when the page is done loading.
+ setTimeout("gRTestURLList.doneURL()", 20);
+ }
+ this.mLoading = false;
+ }
+ },
+
+ onProgressChange : function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ this.mURLBar.value = aLocation.spec;
+ this.setButtonEnabled(this.mForwardButton, gBrowser.canGoForward);
+ this.setButtonEnabled(this.mBackButton, gBrowser.canGoBack);
+ },
+
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ this.mStatusText.value = aMessage;
+ },
+
+ onSecurityChange : function(aWebProgress, aRequest, aState)
+ {
+ },
+
+ // non-interface methods
+ setButtonEnabled : function(aButtonElement, aEnabled)
+ {
+ if (aEnabled)
+ aButtonElement.removeAttribute("disabled");
+ else
+ aButtonElement.setAttribute("disabled", "true");
+ },
+
+ mStatusText : null,
+ mURLBar : null,
+ mForwardButton : null,
+ mBackButton : null,
+ mStopButton : null,
+
+ mLoading : false
+
+}
+
+function OnLDBLoad()
+{
+ gBrowser = document.getElementById("browser");
+
+ gProgressListener = new nsLDBBrowserContentListener();
+ gBrowser.addProgressListener(gProgressListener);
+
+ gDebugger = Components.classes[NS_LAYOUT_DEBUGGINGTOOLS_CONTRACTID].
+ createInstance(nsILayoutDebuggingTools);
+
+ if (window.arguments && window.arguments[0])
+ gBrowser.loadURI(window.arguments[0]);
+ else
+ gBrowser.goHome();
+
+ gDebugger.init(gBrowser.contentWindow);
+
+ checkPersistentMenus();
+ gRTestIndexList = new RTestIndexList();
+}
+
+function checkPersistentMenu(item)
+{
+ var menuitem = document.getElementById("menu_" + item);
+ menuitem.setAttribute("checked", gDebugger[item]);
+}
+
+function checkPersistentMenus()
+{
+ // Restore the toggles that are stored in prefs.
+ checkPersistentMenu("paintFlashing");
+ checkPersistentMenu("paintDumping");
+ checkPersistentMenu("invalidateDumping");
+ checkPersistentMenu("eventDumping");
+ checkPersistentMenu("motionEventDumping");
+ checkPersistentMenu("crossingEventDumping");
+ checkPersistentMenu("reflowCounts");
+}
+
+
+function OnLDBUnload()
+{
+ gBrowser.removeProgressListener(gProgressListener);
+}
+
+function toggle(menuitem)
+{
+ // trim the initial "menu_"
+ var feature = menuitem.id.substring(5);
+ gDebugger[feature] = menuitem.getAttribute("checked") == "true";
+}
+
+function openFile()
+{
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var fp = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(window, "Select a File", nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterHTML | nsIFilePicker.filterAll);
+ if (fp.show() == nsIFilePicker.returnOK && fp.fileURL.spec &&
+ fp.fileURL.spec.length > 0) {
+ gBrowser.loadURI(fp.fileURL.spec);
+ }
+}
+const LDB_RDFNS = "http://mozilla.org/newlayout/LDB-rdf#";
+const NC_RDFNS = "http://home.netscape.com/NC-rdf#";
+
+function RTestIndexList() {
+ this.init();
+}
+
+RTestIndexList.prototype = {
+
+ init : function()
+ {
+ const nsIPrefService = Components.interfaces.nsIPrefService;
+ const PREF_SERVICE_CONTRACTID = "@mozilla.org/preferences-service;1";
+ const PREF_BRANCH_NAME = "layout_debugger.rtest_url.";
+ const nsIRDFService = Components.interfaces.nsIRDFService;
+ const RDF_SERVICE_CONTRACTID = "@mozilla.org/rdf/rdf-service;1";
+ const nsIRDFDataSource = Components.interfaces.nsIRDFDataSource;
+ const RDF_DATASOURCE_CONTRACTID =
+ "@mozilla.org/rdf/datasource;1?name=in-memory-datasource";
+
+ this.mPrefService = Components.classes[PREF_SERVICE_CONTRACTID].
+ getService(nsIPrefService);
+ this.mPrefBranch = this.mPrefService.getBranch(PREF_BRANCH_NAME);
+
+ this.mRDFService = Components.classes[RDF_SERVICE_CONTRACTID].
+ getService(nsIRDFService);
+ this.mDataSource = Components.classes[RDF_DATASOURCE_CONTRACTID].
+ createInstance(nsIRDFDataSource);
+
+ this.mLDB_Root = this.mRDFService.GetResource(LDB_RDFNS + "Root");
+ this.mNC_Name = this.mRDFService.GetResource(NC_RDFNS + "name");
+ this.mNC_Child = this.mRDFService.GetResource(NC_RDFNS + "child");
+
+ this.load();
+
+ document.getElementById("menu_RTest_baseline").database.
+ AddDataSource(this.mDataSource);
+ document.getElementById("menu_RTest_verify").database.
+ AddDataSource(this.mDataSource);
+ document.getElementById("menu_RTest_remove").database.
+ AddDataSource(this.mDataSource);
+ },
+
+ save : function()
+ {
+ this.mPrefBranch.deleteBranch("");
+
+ const nsIRDFLiteral = Components.interfaces.nsIRDFLiteral;
+ const nsIRDFResource = Components.interfaces.nsIRDFResource;
+ var etor = this.mDataSource.GetTargets(this.mLDB_Root,
+ this.mNC_Child, true);
+ var i = 0;
+ while (etor.hasMoreElements()) {
+ var resource = etor.getNext().QueryInterface(nsIRDFResource);
+ var literal = this.mDataSource.GetTarget(resource, this.mNC_Name, true);
+ literal = literal.QueryInterface(nsIRDFLiteral);
+ this.mPrefBranch.setCharPref(i.toString(), literal.Value);
+ ++i;
+ }
+
+ this.mPrefService.savePrefFile(null);
+ },
+
+ load : function()
+ {
+ var prefList = this.mPrefBranch.getChildList("");
+
+ var i = 0;
+ for (var pref in prefList) {
+ var file = this.mPrefBranch.getCharPref(pref);
+ var resource = this.mRDFService.GetResource(file);
+ var literal = this.mRDFService.GetLiteral(file);
+ this.mDataSource.Assert(this.mLDB_Root, this.mNC_Child, resource, true);
+ this.mDataSource.Assert(resource, this.mNC_Name, literal, true);
+ ++i;
+ }
+
+ },
+
+ /* Add a new list of regression tests to the menus. */
+ add : function()
+ {
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+ const NS_FILEPICKER_CONTRACTID = "@mozilla.org/filepicker;1";
+
+ var fp = Components.classes[NS_FILEPICKER_CONTRACTID].
+ createInstance(nsIFilePicker);
+
+ // XXX l10n (but this is just for 5 developers, so no problem)
+ fp.init(window, "New Regression Test List", nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterAll);
+ fp.defaultString = "rtest.lst";
+ if (fp.show() != nsIFilePicker.returnOK)
+ return;
+
+ var file = fp.file.persistentDescriptor;
+ var resource = this.mRDFService.GetResource(file);
+ var literal = this.mRDFService.GetLiteral(file);
+ this.mDataSource.Assert(this.mLDB_Root, this.mNC_Child, resource, true);
+ this.mDataSource.Assert(resource, this.mNC_Name, literal, true);
+
+ this.save();
+
+ },
+
+ remove : function(file)
+ {
+ var resource = this.mRDFService.GetResource(file);
+ var literal = this.mRDFService.GetLiteral(file);
+ this.mDataSource.Unassert(this.mLDB_Root, this.mNC_Child, resource);
+ this.mDataSource.Unassert(resource, this.mNC_Name, literal);
+
+ this.save();
+ },
+
+ mPrefBranch : null,
+ mPrefService : null,
+ mRDFService : null,
+ mDataSource : null,
+ mLDB_Root : null,
+ mNC_Child : null,
+ mNC_Name : null
+}
+
+const nsIFileInputStream = Components.interfaces.nsIFileInputStream;
+const nsILineInputStream = Components.interfaces.nsILineInputStream;
+const nsIFile = Components.interfaces.nsIFile;
+const nsILocalFile = Components.interfaces.nsILocalFile;
+const nsIFileURL = Components.interfaces.nsIFileURL;
+const nsIIOService = Components.interfaces.nsIIOService;
+const nsILayoutRegressionTester = Components.interfaces.nsILayoutRegressionTester;
+
+const NS_LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+const IO_SERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
+const NS_LOCALFILEINPUTSTREAM_CONTRACTID =
+ "@mozilla.org/network/file-input-stream;1";
+
+
+function RunRTest(aFilename, aIsBaseline, aIsPrinting)
+{
+ if (gRTestURLList) {
+ // XXX Does alert work?
+ alert("Already running regression test.\n");
+ return;
+ }
+ dump("Running " + (aIsBaseline?"baseline":"verify") +
+ (aIsPrinting?" PrintMode":"") + " test for " + aFilename + ".\n");
+
+ var listFile = Components.classes[NS_LOCAL_FILE_CONTRACTID].
+ createInstance(nsILocalFile);
+ listFile.persistentDescriptor = aFilename;
+ gRTestURLList = new RTestURLList(listFile, aIsBaseline, aIsPrinting);
+ gRTestURLList.startURL();
+}
+
+function RTestURLList(aLocalFile, aIsBaseline, aIsPrinting) {
+ this.init(aLocalFile, aIsBaseline, aIsPrinting);
+}
+
+RTestURLList.prototype = {
+ init : function(aLocalFile, aIsBaseline, aIsPrinting)
+ {
+ this.mIsBaseline = aIsBaseline;
+ this.mIsPrinting = aIsPrinting;
+ this.mURLs = new Array();
+ this.readFileList(aLocalFile);
+ this.mRegressionTester =
+ Components.classes["@mozilla.org/layout-debug/regressiontester;1"].
+ createInstance(nsILayoutRegressionTester)
+ },
+
+ readFileList : function(aLocalFile)
+ {
+ var ios = Components.classes[IO_SERVICE_CONTRACTID]
+ .getService(nsIIOService);
+ var dirURL = ios.newFileURI(aLocalFile.parent);
+
+ var fis = Components.classes[NS_LOCALFILEINPUTSTREAM_CONTRACTID].
+ createInstance(nsIFileInputStream);
+ fis.init(aLocalFile, -1, -1, false);
+ var lis = fis.QueryInterface(nsILineInputStream);
+
+ var line = {value:null};
+ do {
+ var more = lis.readLine(line);
+ var str = line.value;
+ str = /^[^#]*/.exec(str); // strip everything after "#"
+ str = /\S*/.exec(str); // take the first chunk of non-whitespace
+ if (!str || str == "")
+ continue;
+
+ var item = dirURL.resolve(str);
+ if (item.match(/\/rtest.lst$/)) {
+ var itemurl = ios.newURI(item, null, null);
+ itemurl = itemurl.QueryInterface(nsIFileURL);
+ this.readFileList(itemurl.file);
+ } else {
+ this.mURLs.push( {url:item, dir:aLocalFile.parent, relurl:str} );
+ }
+ } while (more);
+ },
+
+ doneURL : function()
+ {
+ var basename =
+ String(this.mCurrentURL.relurl).replace(/[:=&.\/?]/g, "_") + ".rgd";
+
+ var data = this.mCurrentURL.dir.clone();
+ data.append( this.mIsBaseline ? "baseline" : "verify");
+ if (!data.exists())
+ data.create(nsIFile.DIRECTORY_TYPE, 0777)
+ data.append(basename);
+
+ dump("Writing regression data to " +
+ data.QueryInterface(nsILocalFile).persistentDescriptor + "\n");
+ if (this.mIsPrinting) {
+ this.mRegressionTester.dumpFrameModel(gBrowser.contentWindow, data,
+ nsILayoutRegressionTester.DUMP_FLAGS_MASK_PRINT_MODE);
+ }
+ else {
+ this.mRegressionTester.dumpFrameModel(gBrowser.contentWindow, data, 0);
+ }
+
+
+
+ if (!this.mIsBaseline) {
+ var base_data = this.mCurrentURL.dir.clone();
+ base_data.append("baseline");
+ base_data.append(basename);
+ dump("Comparing to regression data from " +
+ base_data.QueryInterface(nsILocalFile).persistentDescriptor + "\n");
+ var filesDiffer =
+ this.mRegressionTester.compareFrameModels(base_data, data,
+ nsILayoutRegressionTester.COMPARE_FLAGS_BRIEF)
+ dump("Comparison for " + this.mCurrentURL.url + " " +
+ (filesDiffer ? "failed" : "passed") + ".\n");
+ }
+
+ this.mCurrentURL = null;
+
+ this.startURL();
+ },
+
+ startURL : function()
+ {
+ this.mCurrentURL = this.mURLs.shift();
+ if (!this.mCurrentURL) {
+ gRTestURLList = null;
+ return;
+ }
+
+ gBrowser.loadURI(this.mCurrentURL.url);
+ },
+
+ mURLs : null,
+ mCurrentURL : null, // url (string), dir (nsIFileURL), relurl (string)
+ mIsBaseline : null,
+ mRegressionTester : null,
+ mIsPrinting : null
+}
diff --git a/layout/tools/layout-debug/ui/content/layoutdebug.xul b/layout/tools/layout-debug/ui/content/layoutdebug.xul
new file mode 100644
index 000000000..8de04847f
--- /dev/null
+++ b/layout/tools/layout-debug/ui/content/layoutdebug.xul
@@ -0,0 +1,187 @@
+<?xml version="1.0"?>
+<!-- vim: set shiftwidth=2 tabstop=8 expandtab :
+ -
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE window [
+ <!ENTITY % layoutdebugDTD
+ SYSTEM "chrome://layoutdebug/locale/layoutdebug.dtd">
+ %layoutdebugDTD;
+
+ <!ENTITY W3C_RDFNS "http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <!ENTITY NC_RDFNS "http://home.netscape.com/NC-rdf#">
+ <!ENTITY LDB_RDFNS "http://mozilla.org/newlayout/LDB-rdf#">
+]>
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css" ?>
+
+<?xul-overlay href="chrome://global/content/globalOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+
+<!--
+
+ NOTE: Because this window is used for layout regression tests, the
+ persist attribute should never be used on anything. Otherwise there
+ is a risk of running baseline and verify runs under different
+ conditions.
+
+-->
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:rdf="&W3C_RDFNS;"
+ id="main-window"
+ align="stretch"
+ title="&ldb.MainWindow.title;"
+ titlemodifier="&ldb.MainWindow.title;"
+ contenttitlesetting="true"
+ titlemenuseparator=" - "
+ windowtype="mozapp:layoutdebug"
+ onload="OnLDBLoad();"
+ onunload="OnLDBUnload();"
+ width="610" height="450"
+ screenX="4" screenY="4"
+ >
+
+ <script src="chrome://layoutdebug/content/layoutdebug.js"/>
+
+ <commandset id="tasksCommands">
+ <command id="cmd_close" oncommand="window.close();"/>
+ <command id="cmd_quit"/>
+ </commandset>
+
+ <keyset id="tasksKeys">
+ <key id="openFileKb" key="&ldb.Open.commandkey;" oncommand="openFile()" modifiers="accel"/>
+ <key id="key_close"/>
+ <key id="key_quit"/>
+ </keyset>
+
+ <vbox flex="1">
+
+ <toolbox>
+ <menubar id="main-menubar" grippyhidden="true">
+ <menu id="menu_file" label="File" accesskey="F">
+ <menupopup id="menu_FilePopup">
+ <menuitem id="menu_open" label="&ldb.Open.label;" accesskey="&ldb.Open.accesskey;" key="openFileKb" oncommand="openFile()"/>
+ <menuitem id="menu_close" label="Close" accesskey="C" oncommand="window.close();"/>
+ </menupopup>
+ </menu>
+ <menu label="&ldb.RegressionTestMenu.label;"
+ accesskey="&ldb.RegressionTestMenu.accesskey;">
+ <menupopup>
+ <menuitem type="checkbox" id="menu_RTestPrintMode" label="&ldb.RegressionPrintMode.label;" accesskey="&ldb.RegressionPrintMode.accesskey;"/>
+ <menu label="&ldb.RunBaselineMenu.label;"
+ accesskey="&ldb.RunBaselineMenu.accesskey;"
+ id="menu_RTest_baseline"
+ datasources="rdf:null"
+ containment="&NC_RDFNS;child"
+ ref="&LDB_RDFNS;Root">
+ <template>
+ <menupopup>
+ <menuitem uri="rdf:*"
+ label="rdf:&NC_RDFNS;name"
+ name="rdf:&NC_RDFNS;name"
+ oncommand="RunRTest(this.getAttribute('name'), true, document.getElementById('menu_RTestPrintMode').getAttribute('checked'));" />
+ </menupopup>
+ </template>
+ </menu>
+ <menu label="&ldb.RunVerifyMenu.label;"
+ accesskey="&ldb.RunVerifyMenu.accesskey;"
+ id="menu_RTest_verify"
+ datasources="rdf:null"
+ containment="&NC_RDFNS;child"
+ ref="&LDB_RDFNS;Root">
+ <template>
+ <menupopup>
+ <menuitem uri="rdf:*"
+ label="rdf:&NC_RDFNS;name"
+ name="rdf:&NC_RDFNS;name"
+ oncommand="RunRTest(this.getAttribute('name'), false, document.getElementById('menu_RTestPrintMode').getAttribute('checked'));" />
+ </menupopup>
+ </template>
+ </menu>
+ <menuseparator />
+ <menuitem id="menu_AddNewList" label="&ldb.AddNewList.label;" accesskey="&ldb.AddNewList.accesskey;" oncommand="gRTestIndexList.add();" />
+ <menu label="&ldb.RemoveListMenu.label;"
+ accesskey="&ldb.RemoveListMenu.accesskey;"
+ id="menu_RTest_remove"
+ datasources="rdf:null"
+ containment="&NC_RDFNS;child"
+ ref="&LDB_RDFNS;Root">
+ <template>
+ <menupopup>
+ <menuitem uri="rdf:*"
+ label="rdf:&NC_RDFNS;name"
+ name="rdf:&NC_RDFNS;name"
+ oncommand="gRTestIndexList.remove(this.getAttribute('name'));" />
+ </menupopup>
+ </template>
+ </menu>
+ </menupopup>
+ </menu>
+ <menu label="&ldb.ToggleMenu.label;"
+ accesskey="&ldb.ToggleMenu.accesskey;">
+ <menupopup>
+ <menuitem type="checkbox" id="menu_visualDebugging" label="&ldb.visualDebugging.label;" accesskey="&ldb.visualDebugging.accesskey;" oncommand="toggle(this);" />
+ <menuitem type="checkbox" id="menu_visualEventDebugging" label="&ldb.visualEventDebugging.label;" accesskey="&ldb.visualEventDebugging.accesskey;" oncommand="toggle(this);" />
+ <menuseparator />
+ <menuitem type="checkbox" id="menu_paintFlashing" label="&ldb.paintFlashing.label;" accesskey="&ldb.paintFlashing.accesskey;" oncommand="toggle(this);" />
+ <menuitem type="checkbox" id="menu_paintDumping" label="&ldb.paintDumping.label;" accesskey="&ldb.paintDumping.accesskey;" oncommand="toggle(this);" />
+ <menuitem type="checkbox" id="menu_invalidateDumping" label="&ldb.invalidateDumping.label;" accesskey="&ldb.invalidateDumping.accesskey;" oncommand="toggle(this);" />
+ <menuseparator />
+ <menuitem type="checkbox" id="menu_eventDumping" label="&ldb.eventDumping.label;" accesskey="&ldb.eventDumping.accesskey;" oncommand="toggle(this);" />
+ <menuitem type="checkbox" id="menu_motionEventDumping" label="&ldb.motionEventDumping.label;" accesskey="&ldb.motionEventDumping.accesskey;" oncommand="toggle(this);" />
+ <menuitem type="checkbox" id="menu_crossingEventDumping" label="&ldb.crossingEventDumping.label;" accesskey="&ldb.crossingEventDumping.accesskey;" oncommand="toggle(this);" />
+ <menuseparator />
+ <menuitem type="checkbox" id="menu_reflowCounts" label="&ldb.reflowCounts.label;" accesskey="&ldb.reflowCounts.accesskey;" oncommand="toggle(this);" />
+ </menupopup>
+ </menu>
+ <menu label="&ldb.DumpMenu.label;"
+ accesskey="&ldb.DumpMenu.accesskey;">
+ <menupopup>
+ <menuitem id="menu_dumpWebShells" label="&ldb.dumpWebShells.label;" accesskey="&ldb.dumpWebShells.accesskey;" oncommand="gDebugger.dumpWebShells();" />
+ <menuitem id="menu_dumpContent" label="&ldb.dumpContent.label;" accesskey="&ldb.dumpContent.accesskey;" oncommand="gDebugger.dumpContent();" />
+ <menuitem id="menu_dumpFrames" label="&ldb.dumpFrames.label;" accesskey="&ldb.dumpFrames.accesskey;" oncommand="gDebugger.dumpFrames();" />
+ <menuitem id="menu_dumpViews" label="&ldb.dumpViews.label;" accesskey="&ldb.dumpViews.accesskey;" oncommand="gDebugger.dumpViews();" />
+ <menuseparator />
+ <menuitem id="menu_dumpStyleSheets" label="&ldb.dumpStyleSheets.label;" accesskey="&ldb.dumpStyleSheets.accesskey;" oncommand="gDebugger.dumpStyleSheets();" />
+ <menuitem id="menu_dumpStyleContexts" label="&ldb.dumpStyleContexts.label;" accesskey="&ldb.dumpStyleContexts.accesskey;" oncommand="gDebugger.dumpStyleContexts();" />
+ <menuseparator />
+ <menuitem id="menu_dumpReflowStats" label="&ldb.dumpReflowStats.label;" accesskey="&ldb.dumpReflowStats.accesskey;" oncommand="gDebugger.dumpReflowStats();" />
+ </menupopup>
+ </menu>
+ <menu id="tasksMenu"/>
+ <menu id="windowMenu"/>
+ <menu id="menu_Help"/>
+ </menubar>
+
+ <toolbar grippyhidden="true">
+ <toolbarbutton id="back-button" class="toolbarbutton-1"
+ label="&ldb.BackButton.label;"
+ oncommand="gBrowser.goBack();" />
+ <toolbarbutton id="forward-button" class="toolbarbutton-1"
+ label="&ldb.ForwardButton.label;"
+ oncommand="gBrowser.goForward();" />
+ <toolbarbutton id="reload-button" class="toolbarbutton-1"
+ label="&ldb.ReloadButton.label;"
+ oncommand="gBrowser.reload();" />
+ <toolbarbutton id="stop-button" class="toolbarbutton-1"
+ label="&ldb.StopButton.label;"
+ oncommand="gBrowser.stop();" />
+
+ <textbox id="urlbar" flex="1"
+ onkeypress="if (event.keyCode == 13)
+ gBrowser.loadURI(this.value);" />
+ </toolbar>
+ </toolbox>
+
+ <browser flex="1" id="browser" type="content-primary"
+ homepage="about:blank" />
+
+ <hbox>
+ <description id="status-text" value="" />
+ </hbox>
+ </vbox>
+</window>
diff --git a/layout/tools/layout-debug/ui/jar.mn b/layout/tools/layout-debug/ui/jar.mn
new file mode 100644
index 000000000..91924bd91
--- /dev/null
+++ b/layout/tools/layout-debug/ui/jar.mn
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+layoutdebug.jar:
+% content layoutdebug %content/layoutdebug/
+% overlay chrome://browser/content/browser.xul chrome://layoutdebug/content/layoutdebug-overlay.xul
+% overlay chrome://communicator/content/tasksOverlay.xul chrome://layoutdebug/content/layoutdebug-overlay.xul
+% locale layoutdebug en-US %locale/en-US/layoutdebug/
+ content/layoutdebug/layoutdebug.xul (content/layoutdebug.xul)
+ content/layoutdebug/layoutdebug.js (content/layoutdebug.js)
+ content/layoutdebug/layoutdebug-overlay.xul (content/layoutdebug-overlay.xul)
+ locale/en-US/layoutdebug/layoutdebug.dtd (locale/en-US/layoutdebug.dtd)
+ locale/en-US/layoutdebug/layoutdebug-overlay.dtd (locale/en-US/layoutdebug-overlay.dtd)
diff --git a/layout/tools/layout-debug/ui/locale/en-US/layoutdebug-overlay.dtd b/layout/tools/layout-debug/ui/locale/en-US/layoutdebug-overlay.dtd
new file mode 100644
index 000000000..47996b2ec
--- /dev/null
+++ b/layout/tools/layout-debug/ui/locale/en-US/layoutdebug-overlay.dtd
@@ -0,0 +1,8 @@
+<!--
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY ldbCmd.label "Layout Debugger">
+<!ENTITY ldbCmd.accesskey "L">
diff --git a/layout/tools/layout-debug/ui/locale/en-US/layoutdebug.dtd b/layout/tools/layout-debug/ui/locale/en-US/layoutdebug.dtd
new file mode 100644
index 000000000..747ae9ae1
--- /dev/null
+++ b/layout/tools/layout-debug/ui/locale/en-US/layoutdebug.dtd
@@ -0,0 +1,76 @@
+<!--
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY app.name.short "layoutdebug">
+<!ENTITY app.name.long "Layout Debugger">
+<!ENTITY app.version "prototype_a">
+<!ENTITY app.author "mozilla.org">
+
+<!ENTITY ldb.MainWindow.title "Layout Debugger">
+
+<!ENTITY ldb.Open.label "Open File…">
+<!ENTITY ldb.Open.accesskey "o">
+<!ENTITY ldb.Open.commandkey "o">
+
+<!ENTITY ldb.BackButton.label "Back">
+<!ENTITY ldb.ForwardButton.label "Forward">
+<!ENTITY ldb.ReloadButton.label "Reload">
+<!ENTITY ldb.StopButton.label "Stop">
+
+
+<!ENTITY ldb.RegressionTestMenu.label "Regression-Test">
+<!ENTITY ldb.RegressionTestMenu.accesskey "R">
+<!ENTITY ldb.RunBaselineMenu.label "Run Baseline">
+<!ENTITY ldb.RunBaselineMenu.accesskey "B">
+<!ENTITY ldb.RunVerifyMenu.label "Run Verify">
+<!ENTITY ldb.RunVerifyMenu.accesskey "V">
+<!ENTITY ldb.RegressionPrintMode.label "Print Mode">
+<!ENTITY ldb.RegressionPrintMode.accesskey "P">
+<!ENTITY ldb.AddNewList.label "Add New List…">
+<!ENTITY ldb.AddNewList.accesskey "A">
+<!ENTITY ldb.RemoveListMenu.label "Remove List">
+<!ENTITY ldb.RemoveListMenu.accesskey "R">
+
+
+<!ENTITY ldb.ToggleMenu.label "Toggle">
+<!ENTITY ldb.ToggleMenu.accesskey "T">
+
+<!ENTITY ldb.visualDebugging.label "Visual Debugging">
+<!ENTITY ldb.visualDebugging.accesskey "V">
+<!ENTITY ldb.visualEventDebugging.label "Visual Event Debugging">
+<!ENTITY ldb.visualEventDebugging.accesskey "E">
+<!ENTITY ldb.paintFlashing.label "Paint Flashing">
+<!ENTITY ldb.paintFlashing.accesskey "F">
+<!ENTITY ldb.paintDumping.label "Paint Dumping">
+<!ENTITY ldb.paintDumping.accesskey "P">
+<!ENTITY ldb.invalidateDumping.label "Invalidate Dumping">
+<!ENTITY ldb.invalidateDumping.accesskey "I">
+<!ENTITY ldb.eventDumping.label "Event Dumping">
+<!ENTITY ldb.eventDumping.accesskey "E">
+<!ENTITY ldb.motionEventDumping.label "Motion Event Dumping">
+<!ENTITY ldb.motionEventDumping.accesskey "M">
+<!ENTITY ldb.crossingEventDumping.label "Crossing Event Dumping">
+<!ENTITY ldb.crossingEventDumping.accesskey "C">
+<!ENTITY ldb.reflowCounts.label "Reflow Counts">
+<!ENTITY ldb.reflowCounts.accesskey "R">
+
+<!ENTITY ldb.DumpMenu.label "Dump">
+<!ENTITY ldb.DumpMenu.accesskey "D">
+
+<!ENTITY ldb.dumpWebShells.label "Web Shells">
+<!ENTITY ldb.dumpWebShells.accesskey "W">
+<!ENTITY ldb.dumpContent.label "Content">
+<!ENTITY ldb.dumpContent.accesskey "C">
+<!ENTITY ldb.dumpFrames.label "Frames">
+<!ENTITY ldb.dumpFrames.accesskey "F">
+<!ENTITY ldb.dumpViews.label "Views and Widgets">
+<!ENTITY ldb.dumpViews.accesskey "V">
+<!ENTITY ldb.dumpStyleSheets.label "Style Sheets">
+<!ENTITY ldb.dumpStyleSheets.accesskey "S">
+<!ENTITY ldb.dumpStyleContexts.label "Style Contexts">
+<!ENTITY ldb.dumpStyleContexts.accesskey "x">
+<!ENTITY ldb.dumpReflowStats.label "Reflow Statistics">
+<!ENTITY ldb.dumpReflowStats.accesskey "R">
diff --git a/layout/tools/layout-debug/ui/moz.build b/layout/tools/layout-debug/ui/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/layout/tools/layout-debug/ui/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
diff --git a/layout/tools/recording/Makefile.in b/layout/tools/recording/Makefile.in
new file mode 100644
index 000000000..1c11a44b4
--- /dev/null
+++ b/layout/tools/recording/Makefile.in
@@ -0,0 +1,19 @@
+# vim: set shiftwidth=8 tabstop=8 autoindent noexpandtab copyindent:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+ recording-cmdline.js \
+ $(NULL)
+
+EXTRA_COMPONENTS += recording-cmdline.manifest
+
+include $(topsrcdir)/config/rules.mk
diff --git a/layout/tools/recording/jar.mn b/layout/tools/recording/jar.mn
new file mode 100644
index 000000000..89923578f
--- /dev/null
+++ b/layout/tools/recording/jar.mn
@@ -0,0 +1,4 @@
+recording.jar:
+% content recording %content/
+ content/recording.xul (recording.xul)
+ content/recording.js (recording.js)
diff --git a/layout/tools/recording/moz.build b/layout/tools/recording/moz.build
new file mode 100644
index 000000000..9c56aa8c1
--- /dev/null
+++ b/layout/tools/recording/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MODULE = 'recording'
diff --git a/layout/tools/recording/recording-cmdline.js b/layout/tools/recording/recording-cmdline.js
new file mode 100644
index 000000000..ec0e8343a
--- /dev/null
+++ b/layout/tools/recording/recording-cmdline.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const nsISupports = Components.interfaces.nsISupports;
+
+const nsICommandLine = Components.interfaces.nsICommandLine;
+const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
+const nsISupportsString = Components.interfaces.nsISupportsString;
+const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
+
+function RecordingCmdLineHandler() {}
+RecordingCmdLineHandler.prototype =
+{
+ classID: Components.ID('{86FB70EC-90FF-45AD-A1C1-F77D3C1184E9}'),
+
+ /* nsISupports */
+ QueryInterface: XPCOMUtils.generateQI([nsICommandLineHandler]),
+
+ /* nsICommandLineHandler */
+ handle : function handler_handle(cmdLine) {
+ var args = { };
+ args.wrappedJSObject = args;
+ try {
+ var uristr = cmdLine.handleFlagWithParam("recording", false);
+ if (uristr == null)
+ return;
+ try {
+ args.uri = cmdLine.resolveURI(uristr).spec;
+ }
+ catch (e) {
+ return;
+ }
+ }
+ catch (e) {
+ cmdLine.handleFlag("recording", true);
+ }
+
+ /**
+ * Manipulate preferences by adding to the *default* branch. Adding
+ * to the default branch means the changes we make won't get written
+ * back to user preferences.
+ *
+ * We want to do this here rather than in reftest.js because it's
+ * important to set the recording pref before the platform Init gets
+ * called.
+ */
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefService);
+ var branch = prefs.getDefaultBranch("");
+
+ try {
+ var outputstr = cmdLine.handleFlagWithParam("recording-output", false);
+ if (outputstr != null) {
+ branch.setCharPref("gfx.2d.recordingfile", outputstr);
+ }
+ } catch (e) { }
+
+ branch.setBoolPref("gfx.2d.recording", true);
+
+ var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(nsIWindowWatcher);
+ wwatch.openWindow(null, "chrome://recording/content/recording.xul", "_blank",
+ "chrome,dialog=no,all", args);
+ cmdLine.preventDefault = true;
+ },
+
+ helpInfo : " -recording <file> Record drawing for a given URL.\n" +
+ " -recording-output <file> Specify destination file for a drawing recording.\n"
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RecordingCmdLineHandler]);
diff --git a/layout/tools/recording/recording-cmdline.manifest b/layout/tools/recording/recording-cmdline.manifest
new file mode 100644
index 000000000..7b4216721
--- /dev/null
+++ b/layout/tools/recording/recording-cmdline.manifest
@@ -0,0 +1,3 @@
+component {86FB70EC-90FF-45AD-A1C1-F77D3C1184E9} recording-cmdline.js
+contract @mozilla.org/commandlinehandler/general-startup;1?type=recording {86FB70EC-90FF-45AD-A1C1-F77D3C1184E9}
+category command-line-handler m-recording @mozilla.org/commandlinehandler/general-startup;1?type=recording
diff --git a/layout/tools/recording/recording.js b/layout/tools/recording/recording.js
new file mode 100644
index 000000000..00215915c
--- /dev/null
+++ b/layout/tools/recording/recording.js
@@ -0,0 +1,53 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- /
+/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CR = Components.results;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const NS_GFXINFO_CONTRACTID = "@mozilla.org/gfx/info;1";
+
+var gContainingWindow = null;
+
+var gBrowser;
+
+function OnDocumentLoad() {
+ gContainingWindow.close();
+}
+
+this.OnRecordingLoad = function OnRecordingLoad(win) {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+
+ if (win === undefined || win == null) {
+ win = window;
+ }
+ if (gContainingWindow == null && win != null) {
+ gContainingWindow = win;
+ }
+
+ gBrowser = gContainingWindow.document.getElementById("browser");
+
+ var gfxInfo = (NS_GFXINFO_CONTRACTID in CC) && CC[NS_GFXINFO_CONTRACTID].getService(CI.nsIGfxInfo);
+ var info = gfxInfo.getInfo();
+ dump(info.AzureContentBackend);
+ if (info.AzureContentBackend == "none") {
+ alert("Page recordings may only be made with Azure content enabled.");
+ gContainingWindow.close();
+ return;
+ }
+
+ gContainingWindow.document.addEventListener("load", OnDocumentLoad, true);
+
+ var args = window.arguments[0].wrappedJSObject;
+
+ gBrowser.loadURI(args.uri);
+}
+
+function OnRecordingUnload() {
+}
diff --git a/layout/tools/recording/recording.xul b/layout/tools/recording/recording.xul
new file mode 100644
index 000000000..189ea25e2
--- /dev/null
+++ b/layout/tools/recording/recording.xul
@@ -0,0 +1,22 @@
+<!-- vim: set shiftwidth=4 tabstop=8 autoindent expandtab: -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet type="text/css" href="data:text/css,
+
+%23_box_windowsDefaultTheme:-moz-system-metric(windows-default-theme) {
+ display: none;
+}
+
+" ?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="recording-window"
+ hidechrome="true"
+ onload="OnRecordingLoad();"
+ onunload="OnRecordingUnload();"
+ style="background:white; overflow:hidden; width:800px; height:600px;"
+ >
+ <script type="application/ecmascript" src="recording.js" />
+ <browser id="browser" type="content-primary" style="min-width: 1024px; min-height: 768px; max-width: 1024px; max-height: 768px"/>
+</window>
diff --git a/layout/tools/reftest/Makefile.in b/layout/tools/reftest/Makefile.in
new file mode 100644
index 000000000..a5721b1f0
--- /dev/null
+++ b/layout/tools/reftest/Makefile.in
@@ -0,0 +1,94 @@
+# vim: set shiftwidth=8 tabstop=8 autoindent noexpandtab copyindent:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS= \
+ reftest-cmdline.js \
+ $(NULL)
+
+ifdef XPI_NAME
+NO_JS_MANIFEST = 1
+DIST_FILES = install.rdf
+
+ifeq ($(MOZ_BUILD_APP),mobile/android)
+DEFINES += -DBOOTSTRAP
+DIST_FILES += bootstrap.js
+endif
+
+ifeq ($(MOZ_BUILD_APP),b2g)
+DEFINES += -DBOOTSTRAP
+DEFINES += -DREFTEST_B2G
+endif
+
+# Used in install.rdf
+USE_EXTENSION_MANIFEST=1
+else
+EXTRA_COMPONENTS += reftest-cmdline.manifest
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+# We're installing to _tests/reftest
+TARGET_DEPTH = ../..
+include $(topsrcdir)/build/automation-build.mk
+
+_DEST_DIR = $(DEPTH)/_tests/reftest
+
+# We want to get an extension-packaged version of reftest as well,
+# so this seems to be the simplest way to make that happen.
+ifndef XPI_NAME
+make-xpi:
+ +$(MAKE) -C $(DEPTH)/netwerk/test/httpserver libs XPI_NAME=reftest
+ +$(MAKE) libs XPI_NAME=reftest
+copy-harness: make-xpi
+libs:: copy-harness
+endif
+
+_HARNESS_FILES = \
+ $(srcdir)/runreftest.py \
+ $(srcdir)/remotereftest.py \
+ $(srcdir)/runreftestb2g.py \
+ $(srcdir)/b2g_start_script.js \
+ automation.py \
+ $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanager.py \
+ $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py \
+ $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py \
+ $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/droid.py \
+ $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/Zeroconf.py \
+ $(topsrcdir)/build/mobile/b2gautomation.py \
+ $(topsrcdir)/build/automationutils.py \
+ $(topsrcdir)/build/mobile/remoteautomation.py \
+ $(topsrcdir)/testing/mochitest/server.js \
+ $(topsrcdir)/build/pgo/server-locations.txt \
+ $(NULL)
+
+$(_DEST_DIR):
+ $(NSINSTALL) -D $@
+
+$(_HARNESS_FILES): $(_DEST_DIR)
+
+# copy harness and the reftest extension bits to $(_DEST_DIR)
+copy-harness: $(_HARNESS_FILES)
+ $(INSTALL) $(_HARNESS_FILES) $(_DEST_DIR)
+ (cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - reftest) | (cd $(_DEST_DIR) && tar -xf -)
+
+PKG_STAGE = $(DIST)/test-package-stage
+
+# stage harness and tests for packaging
+stage-package:
+ $(NSINSTALL) -D $(PKG_STAGE)/reftest && $(NSINSTALL) -D $(PKG_STAGE)/reftest/tests
+ (cd $(DEPTH)/_tests/reftest/ && tar $(TAR_CREATE_FLAGS_QUIET) - *) | (cd $(PKG_STAGE)/reftest && tar -xf -)
+ $(PYTHON) $(topsrcdir)/layout/tools/reftest/print-manifest-dirs.py \
+ $(topsrcdir) \
+ $(topsrcdir)/layout/reftests/reftest.list \
+ $(topsrcdir)/testing/crashtest/crashtests.list \
+ | (cd $(topsrcdir) && xargs tar $(TAR_CREATE_FLAGS_QUIET) -) \
+ | (cd $(PKG_STAGE)/reftest/tests && tar -xf -)
diff --git a/layout/tools/reftest/README.txt b/layout/tools/reftest/README.txt
new file mode 100644
index 000000000..2ade610a2
--- /dev/null
+++ b/layout/tools/reftest/README.txt
@@ -0,0 +1,510 @@
+Layout Engine Visual Tests (reftest)
+L. David Baron <dbaron@dbaron.org>, Mozilla Corporation
+July 19, 2006
+
+This code is designed to run tests of Mozilla's layout engine. These
+tests consist of an HTML (or other format) file along with a reference
+in the same format. The tests are run based on a manifest file, and for
+each test, PASS or FAIL is reported, and UNEXPECTED is reported if the
+result (PASS or FAIL) was not the expected result noted in the manifest.
+
+Why this way?
+=============
+
+Writing HTML tests where the reference rendering is also in HTML is
+harder than simply writing bits of HTML that can be regression-tested by
+comparing the rendering of an older build to that of a newer build
+(perhaps using stored reference images from the older build). However,
+comparing across time has major disadvantages:
+
+ * Comparisons across time either require two runs for every test, or
+ they require stored reference images appropriate for the platform and
+ configuration (often limiting testing to a very specific
+ configuration).
+
+ * Comparisons across time may fail due to expected changes, for
+ example, changes in the default style sheet for HTML, changes in the
+ appearance of form controls, or changes in default preferences like
+ default font size or default colors.
+
+Using tests for which the pass criteria were explicitly chosen allows
+running tests at any time to see whether they still pass.
+
+Manifest Format
+===============
+
+The test manifest format is a plain text file. A line starting with a
+"#" is a comment. Lines may be commented using whitespace followed by
+a "#" and the comment. Each non-blank line (after removal of comments)
+must be one of the following:
+
+1. Inclusion of another manifest
+
+ <failure-type>* include <relative_path>
+
+ <failure-type> is the same as listed below for a test item. As for
+ test items, multiple failure types listed on the same line are
+ combined by using the last matching failure type listed. However,
+ the failure type on a manifest is combined with the failure type on
+ the test (or on a nested manifest) with the rule that the last in the
+ following list wins: fails, random, skip. (In other words, skip
+ always wins, and random beats fails.)
+
+2. A test item
+
+ [ <failure-type> | <preference> ]* [<http>] <type> <url> <url_ref>
+
+ where
+
+ a. <failure-type> (optional) is one of the following:
+
+ fails The test passes if the images of the two renderings DO NOT
+ meet the conditions specified in the <type>.
+
+ fails-if(condition) If the condition is met, the test passes if the
+ images of the two renderings DO NOT meet the
+ conditions of <type>. If the condition is not met,
+ the test passes if the conditions of <type> are met.
+
+ needs-focus The test fails or times out if the reftest window is not
+ focused.
+
+ random The results of the test are random and therefore not to be
+ considered in the output.
+
+ random-if(condition) The results of the test are random if a given
+ condition is met.
+
+ silentfail This test may fail silently, and if that happens it should
+ count as if the test passed. This is useful for cases where
+ silent failure is the intended behavior (for example, in
+ an out of memory situation in JavaScript, we stop running
+ the script silently and immediately, in hopes of reclaiming
+ enough memory to keep the browser functioning).
+
+ silentfail-if(condition) This test may fail silently if the condition
+ is met.
+
+ skip This test should not be run. This is useful when a test fails in a
+ catastrophic way, such as crashing or hanging the browser. Using
+ 'skip' is preferred to simply commenting out the test because we
+ want to report the test failure at the end of the test run.
+
+ skip-if(condition) If the condition is met, the test is not run. This is
+ useful if, for example, the test crashes only on a
+ particular platform (i.e. it allows us to get test
+ coverage on the other platforms).
+
+ slow The test may take a long time to run, so run it if slow tests are
+ either enabled or not disabled (test manifest interpreters may
+ choose whether or not to run such tests by default).
+
+ slow-if(condition) If the condition is met, the test is treated as if
+ 'slow' had been specified. This is useful for tests
+ which are slow only on particular platforms (e.g. a
+ test which exercised out-of-memory behavior might be
+ fast on a 32-bit system but inordinately slow on a
+ 64-bit system).
+
+ fuzzy(maxDiff, diffCount)
+ This allows a test to pass if the pixel value differences are <=
+ maxDiff and the total number of different pixels is <= diffCount.
+ It can also be used with '!=' to ensure that the difference is
+ greater than maxDiff.
+
+ fuzzy-if(condition, maxDiff, diffCount)
+ If the condition is met, the test is treated as if 'fuzzy' had been
+ specified. This is useful if there are differences on particular
+ platforms.
+
+ require-or(cond1&&cond2&&...,fallback)
+ Require some particular setup be performed or environmental
+ condition(s) made true (eg setting debug mode) before the test
+ is run. If any condition is unknown, unimplemented, or fails,
+ revert to the fallback failure-type.
+ Example: require-or(debugMode,skip)
+
+ asserts(count)
+ Loading the test and reference is known to assert exactly
+ count times.
+ NOTE: An asserts() notation with a non-zero count or maxCount
+ suppresses use of a cached canvas for the test with the
+ annotation. However, if later occurrences of the same test
+ are not annotated, they will use the cached canvas
+ (potentially from the load that asserted). This allows
+ repeated use of the same test or reference to be annotated
+ correctly (which may be particularly useful when the uses are
+ in different subdirectories that can be tested independently),
+ but does not force them to be, nor does it force suppression
+ of caching for a common reference when it is the test that
+ asserts.
+
+ asserts(minCount-maxCount)
+ Loading the test and reference is known to assert between
+ minCount and maxCount times, inclusive.
+ NOTE: See above regarding canvas caching.
+
+ asserts-if(condition,count)
+ asserts-if(condition,minCount-maxCount)
+ Same as above, but only if condition is true.
+
+ Conditions are JavaScript expressions *without spaces* in them.
+ They are evaluated in a sandbox in which a limited set of
+ variables are defined. See the BuildConditionSandbox function in
+ layout/tools/reftest.js for details.
+
+ Examples of using conditions:
+ fails-if(winWidget) == test reference
+ asserts-if(cocoaWidget,2) load crashtest
+
+ b. <preference> (optional) is a string of the form
+
+ pref(<name>,<value>)
+ test-pref(<name>,<value>)
+ ref-pref(<name>,<value>)
+
+ where <name> is the name of a preference setting, as seen in
+ about:config, and <value> is the value to which this preference should
+ be set. <value> may be a boolean (true/false), an integer, or a
+ quoted string *without spaces*, according to the type of the preference.
+
+ The preference will be set to the specified value prior to
+ rendering the test and/or reference canvases (pref() applies to
+ both, test-pref() only to the test, and ref-pref() only to the
+ reference), and will be restored afterwards so that following
+ tests are not affected. Note that this feature is only useful for
+ "live" preferences that take effect immediately, without requiring
+ a browser restart.
+
+ c. <http>, if present, is one of the strings (sans quotes) "HTTP" or
+ "HTTP(..)" or "HTTP(../..)" or "HTTP(../../..)", etc. , indicating that
+ the test should be run over an HTTP server because it requires certain
+ HTTP headers or a particular HTTP status. (Don't use this if your test
+ doesn't require this functionality, because it unnecessarily slows down
+ the test.)
+
+ With "HTTP", HTTP tests have the restriction that any resource an HTTP
+ test accesses must be accessed using a relative URL, and the test and
+ the resource must be within the directory containing the reftest
+ manifest that describes the test (or within a descendant directory).
+ The variants "HTTP(..)", etc., can be used to relax this restriction by
+ allowing resources in the parent directory, etc.
+
+ To modify the HTTP status or headers of a resource named FOO, create a
+ sibling file named FOO^headers^ with the following contents:
+
+ [<http-status>]
+ <http-header>*
+
+ <http-status> A line of the form "HTTP ###[ <description>]", where
+ ### indicates the desired HTTP status and <description>
+ indicates a desired HTTP status description, if any.
+ If this line is omitted, the default is "HTTP 200 OK".
+ <http-header> A line in standard HTTP header line format, i.e.
+ "Field-Name: field-value". You may not repeat the use
+ of a Field-Name and must coalesce such headers together,
+ and each header must be specified on a single line, but
+ otherwise the format exactly matches that from HTTP
+ itself.
+
+ HTTP tests may also incorporate SJS files. SJS files provide similar
+ functionality to CGI scripts, in that the response they produce can be
+ dependent on properties of the incoming request. Currently these
+ properties are restricted to method type and headers, but eventually
+ it should be possible to examine data in the body of the request as
+ well when computing the generated response. An SJS file is a JavaScript
+ file with a .sjs extension which defines a global |handleRequest|
+ function (called every time that file is loaded during reftests) in this
+ format:
+
+ function handleRequest(request, response)
+ {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ // You *probably* want this, or else you'll get bitten if you run
+ // reftest multiple times with the same profile.
+ response.setHeader("Cache-Control", "no-cache");
+
+ response.write("any ASCII data you want");
+
+ var outputStream = response.bodyOutputStream;
+ // ...anything else you want to do, synchronously...
+ }
+
+ For more details on exactly which functions and properties are available
+ on request/response in handleRequest, see the nsIHttpRe(quest|sponse)
+ definitions in <netwerk/test/httpserver/nsIHttpServer.idl>.
+
+ d. <type> is one of the following:
+
+ == The test passes if the images of the two renderings are the
+ SAME.
+ != The test passes if the images of the two renderings are
+ DIFFERENT.
+ load The test passes unconditionally if the page loads. url_ref
+ must be omitted, and the test cannot be marked as fails or
+ random. (Used to test for crashes, hangs, assertions, and
+ leaks.)
+ script The loaded page records the test's pass or failure status
+ in a JavaScript data structure accessible through the following
+ API.
+
+ getTestCases() returns an array of test result objects
+ representing the results of the tests performed by the page.
+
+ Each test result object has two methods:
+
+ testPassed() returns true if the test result object passed,
+ otherwise it returns false.
+
+ testDescription() returns a string describing the test
+ result.
+
+ url_ref must be omitted. The test may be marked as fails or
+ random. (Used to test the JavaScript Engine.)
+
+ e. <url> is either a relative file path or an absolute URL for the
+ test page
+
+ f. <url_ref> is either a relative file path or an absolute URL for
+ the reference page
+
+ The only difference between <url> and <url_ref> is that results of
+ the test are reported using <url> only.
+
+3. Specification of a url prefix
+
+ url-prefix <string>
+
+ <string> will be prepended to relative <url> and <url_ref> for all following
+ test items in the manifest.
+
+ <string> will not be prepended to the relative path when including another
+ manifest, e.g. include <relative_path>.
+
+ <string> will not be prepended to any <url> or <url_ref> matching the pattern
+ /^\w+:/. This will prevent the prefix from being applied to any absolute url
+ containing a protocol such as data:, about:, or http:.
+
+ While the typical use of url-prefix is expected to be as the first line of
+ a manifest, it is legal to use it anywhere in a manifest. Subsequent uses
+ of url-prefix overwrite any existing values.
+
+4. Specification of default preferences
+
+ default-preferences <preference>*
+
+ where <preference> is defined above.
+
+ The <preference> settings will be used for all following test items in the
+ manifest.
+
+ If a test item includes its own preference settings, then they will override
+ any settings for preferences of the same names that are set using
+ default-preferences, just as later items within a line override earlier ones.
+
+ A default-preferences line with no <preference> settings following it will
+ reset the set of default preferences to be empty.
+
+ As with url-prefix, default-preferences will often be used at the start of a
+ manifest file so that it applies to all test items, but it is legal for
+ default-preferences to appear anywhere in the manifest. A subsequent
+ default-preferences will reset any previous default preference values and
+ overwrite them with the specified <preference> values.
+
+This test manifest format could be used by other harnesses, such as ones
+that do not depend on XUL, or even ones testing other layout engines.
+
+Running Tests
+=============
+
+(If you're not using a DEBUG build, first set browser.dom.window.dump.enabled
+to true (in about:config, in the profile you'll be using to run the tests).
+Create the option as a new boolean if it doesn't exist already. If you skip
+this step you won't get any output in the terminal.)
+
+At some point in the future there will hopefully be a cleaner way to do
+this. For now, go to your object directory, and run (perhaps using
+MOZ_NO_REMOTE=1 or the -profile <directory> option)
+
+./firefox -reftest /path/to/srcdir/mozilla/layout/reftests/reftest.list > reftest.out
+
+and then search/grep reftest.out for "UNEXPECTED".
+
+There are two scripts provided to convert the reftest.out to HTML.
+clean-reftest-output.pl converts reftest.out into simple HTML, stripping
+lines from the log that aren't relevant. reftest-to-html.pl converts
+the output into html that makes it easier to visually check for
+failures.
+
+Testable Areas
+==============
+
+This framework is capable of testing many areas of the layout engine.
+It is particularly well-suited to testing dynamic change handling (by
+comparison to the static end-result as a reference) and incremental
+layout (comparison of a script-interrupted layout to one that was not).
+However, it is also possible to write tests for many other things that
+can be described in terms of equivalence, for example:
+
+ * CSS cascading could be tested by comparing the result of a
+ complicated set of style rules that makes a word green to <span
+ style="color:green">word</span>.
+
+ * <canvas> compositing operators could be tested by comparing the
+ result of drawing using canvas to a block-level element with the
+ desired color as a CSS background-color.
+
+ * CSS counters could be tested by comparing the text output by counters
+ with a page containing the text written out
+
+ * complex margin collapsing could be tested by comparing the complex
+ case to a case where the margin is written out, or where the margin
+ space is created by an element with 'height' and transparent
+ background
+
+When it is not possible to test by equivalence, it may be possible to
+test by non-equivalence. For example, testing justification in cases
+with more than two words, or more than three different words, is
+difficult. However, it is simple to test that justified text is at
+least displayed differently from left-, center-, or right-aligned text.
+
+Writing Tests
+=============
+
+When writing tests for this framework, it is important for the test to
+depend only on behaviors that are known to be correct and permanent.
+For example, tests should not depend on default font sizes, default
+margins of the body element, the default style sheet used for HTML, the
+default appearance of form controls, or anything else that can be
+avoided.
+
+In general, the best way to achieve this is to make the test and the
+reference identical in as many aspects as possible. For example:
+
+ Good test markup:
+ <div style="color:green"><table><tr><td><span>green
+ </span></td></tr></table></div>
+
+ Good reference markup:
+ <div><table><tr><td><span style="color:green">green
+ </span></td></tr></table></div>
+
+ BAD reference markup:
+ <!-- 3px matches the default cellspacing and cellpadding -->
+ <div style="color:green; padding: 3px">green
+ </div>
+
+ BAD test markup:
+ <!-- span doesn't change the positioning, so skip it -->
+ <div style="color:green"><table><tr><td>green
+ </td></tr></table></div>
+
+Asynchronous Tests
+==================
+
+Normally reftest takes a snapshot of the given markup's rendering right
+after the load event fires for content. If your test needs to postpone
+the moment the snapshot is taken, it should make sure a class
+'reftest-wait' is on the root element by the moment the load event
+fires. The easiest way to do this is to put it in the markup, e.g.:
+ <html class="reftest-wait">
+
+When your test is ready, you should remove this class from the root
+element, for example using this code:
+ document.documentElement.className = "";
+
+
+Note that in layout tests it is often enough to trigger layout using
+ document.body.offsetWidth // HTML example
+
+When possible, you should use this technique instead of making your
+test async.
+
+Invalidation Tests
+==================
+
+When a test (or reference) uses reftest-wait, reftest tracks invalidation
+via MozAfterPaint and updates the test image in the same way that
+a regular window would be repainted. Therefore it is possible to test
+invalidation-related bugs by setting up initial content and then
+dynamically modifying it before removing reftest-wait. However, it is
+important to get the timing of these dynamic modifications right so that
+the test doesn't accidentally pass because a full repaint of the window
+was already pending. To help with this, reftest fires one MozReftestInvalidate
+event at the document root element for a reftest-wait test when it is safe to
+make changes that should test invalidation. The event bubbles up to the
+document and window so you can set listeners there too. For example,
+
+function doTest() {
+ document.body.style.border = "";
+ document.documentElement.removeAttribute('class');
+}
+document.addEventListener("MozReftestInvalidate", doTest, false);
+
+Painting Tests
+==============
+
+If an element shouldn't be painted, set the class "reftest-no-paint" on it
+when doing an invalidation test. Causing a repaint in your
+MozReftestInvalidate handler (for example, by changing the body's background
+colour) will accurately test whether the element is painted.
+
+Zoom Tests
+==========
+
+When the root element of a test has a "reftest-zoom" attribute, that zoom
+factor is applied when rendering the test. The reftest document will be
+800 device pixels wide by 1000 device pixels high. The reftest harness assumes
+that the CSS pixel dimensions are 800/zoom and 1000/zoom. For best results
+therefore, choose zoom factors that do not require rounding when we calculate
+the number of appunits per device pixel; i.e. the zoom factor should divide 60,
+so 60/zoom is an integer.
+
+Printing Tests
+==============
+
+Now that the patch for bug 374050 has landed
+(https://bugzilla.mozilla.org/show_bug.cgi?id=374050), it is possible to
+create reftests that run in a paginated context.
+
+The page size used is 5in wide and 3in tall (with the default half-inch
+margins). This is to allow tests to have less text and to make the
+entire test fit on the screen.
+
+There is a layout/reftests/printing directory for printing reftests; however,
+there is nothing special about this directory. You can put printing reftests
+anywhere that is appropriate.
+
+The suggested first lines for any printing test is
+<!DOCTYPE html><html class="reftest-print">
+<style>html{font-size:12pt}</style>
+
+The reftest-print class on the root element triggers the reftest to
+switch into page mode on load. Fixing the font size is suggested,
+although not required, because the pages are a fixed size in inches.
+
+The underlying layout support for this mode isn't really complete; it
+doesn't use exactly the same codepath as real print preview/print. In
+particular, scripting and frames are likely to cause problems; it is untested,
+though. That said, it should be sufficient for testing layout issues related
+to pagination.
+
+Plugin and IPC Process Crash Tests
+==================================
+
+If you are running a test that causes an out-of-process plugin or IPC process
+under Electrolysis to crash as part of a reftest, this will cause process
+crash minidump files to be left in the profile directory. The test
+infrastructure that runs the reftests will notice these minidump files and
+dump out information from them, and these additional error messages in the logs
+can end up erroneously being associated with other errors from the reftest run.
+They are also confusing, since the appearance of "PROCESS-CRASH" messages in
+the test run output can seem like a real problem, when in fact it is the
+expected behavior.
+
+To indicate to the reftest framework that a test is expecting a plugin or
+IPC process crash, have the test include "reftest-expect-process-crash" as
+one of the root element's classes by the time the test has finished. This will
+cause any minidump files that are generated while running the test to be removed
+and they won't cause any error messages in the test run output.
diff --git a/layout/tools/reftest/b2g_start_script.js b/layout/tools/reftest/b2g_start_script.js
new file mode 100644
index 000000000..87ff726cf
--- /dev/null
+++ b/layout/tools/reftest/b2g_start_script.js
@@ -0,0 +1,47 @@
+args = __marionetteParams;
+
+function setDefaultPrefs() {
+ // This code sets the preferences for extension-based reftest; for
+ // command-line based reftest they are set in function handler_handle in
+ // reftest-cmdline.js. These two locations should stay in sync.
+ //
+ // FIXME: These should be in only one place.
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefService);
+ var branch = prefs.getDefaultBranch("");
+ branch.setBoolPref("dom.use_xbl_scopes_for_remote_xul", false);
+ branch.setBoolPref("gfx.color_management.force_srgb", true);
+ branch.setBoolPref("browser.dom.window.dump.enabled", true);
+ branch.setIntPref("ui.caretBlinkTime", -1);
+ branch.setBoolPref("dom.send_after_paint_to_content", true);
+ // no slow script dialogs
+ branch.setIntPref("dom.max_script_run_time", 0);
+ branch.setIntPref("dom.max_chrome_script_run_time", 0);
+ branch.setIntPref("hangmonitor.timeout", 0);
+ // Ensure autoplay is enabled for all platforms.
+ branch.setBoolPref("media.autoplay.enabled", true);
+ // Disable updates
+ branch.setBoolPref("app.update.enabled", false);
+}
+
+function setPermissions(webserver, port) {
+ var perms = Components.classes["@mozilla.org/permissionmanager;1"]
+ .getService(Components.interfaces.nsIPermissionManager);
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var uri = ioService.newURI("http://" + webserver + ":" + port, null, null);
+ perms.add(uri, "allowXULXBL", Components.interfaces.nsIPermissionManager.ALLOW_ACTION);
+}
+
+// Load into any existing windows
+let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+let win = wm.getMostRecentWindow('');
+
+// Set preferences and permissions
+setDefaultPrefs();
+setPermissions(args[0], args[1]);
+
+// Start the reftests
+Components.utils.import("chrome://reftest/content/reftest.jsm");
+OnRefTestLoad(win);
diff --git a/layout/tools/reftest/bootstrap.js b/layout/tools/reftest/bootstrap.js
new file mode 100644
index 000000000..36ab76cc3
--- /dev/null
+++ b/layout/tools/reftest/bootstrap.js
@@ -0,0 +1,101 @@
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+function loadIntoWindow(window) {}
+function unloadFromWindow(window) {}
+
+function setDefaultPrefs() {
+ // This code sets the preferences for extension-based reftest; for
+ // command-line based reftest they are set in function handler_handle in
+ // reftest-cmdline.js. These two locations should stay in sync.
+ //
+ // FIXME: These should be in only one place.
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefService);
+ var branch = prefs.getDefaultBranch("");
+ // For mochitests, we're more interested in testing the behavior of in-
+ // content XBL bindings, so we set this pref to true. In reftests, we're
+ // more interested in testing the behavior of XBL as it works in chrome,
+ // so we want this pref to be false.
+ branch.setBoolPref("dom.use_xbl_scopes_for_remote_xul", false);
+ branch.setBoolPref("gfx.color_management.force_srgb", true);
+ branch.setBoolPref("browser.dom.window.dump.enabled", true);
+ branch.setIntPref("ui.caretBlinkTime", -1);
+ branch.setBoolPref("dom.send_after_paint_to_content", true);
+ // no slow script dialogs
+ branch.setIntPref("dom.max_script_run_time", 0);
+ branch.setIntPref("dom.max_chrome_script_run_time", 0);
+ branch.setIntPref("hangmonitor.timeout", 0);
+ // Ensure autoplay is enabled for all platforms.
+ branch.setBoolPref("media.autoplay.enabled", true);
+ // Disable updates
+ branch.setBoolPref("app.update.enabled", false);
+ // Disable addon updates and prefetching so we don't leak them
+ branch.setBoolPref("extensions.update.enabled", false);
+ branch.setBoolPref("extensions.getAddons.cache.enabled", false);
+ // Disable blocklist updates so we don't have them reported as leaks
+ branch.setBoolPref("extensions.blocklist.enabled", false);
+ // Make url-classifier updates so rare that they won't affect tests
+ branch.setIntPref("urlclassifier.updateinterval", 172800);
+ // Disable high-quality downscaling, since it makes reftests more difficult.
+ branch.setBoolPref("image.high_quality_downscaling.enabled", false);
+}
+
+var windowListener = {
+ onOpenWindow: function(aWindow) {
+ let domWindow = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowInternal || Components.interfaces.nsIDOMWindow);
+ domWindow.addEventListener("load", function() {
+ domWindow.removeEventListener("load", arguments.callee, false);
+
+ let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
+
+ // Load into any existing windows
+ let enumerator = wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext().QueryInterface(Components.interfaces.nsIDOMWindow);
+ setDefaultPrefs();
+ Components.utils.import("chrome://reftest/content/reftest.jsm");
+ win.addEventListener("pageshow", function() {
+ win.removeEventListener("pageshow", arguments.callee);
+ // We add a setTimeout here because windows.innerWidth/Height are not set yet;
+ win.setTimeout(function () {OnRefTestLoad(win);}, 0);
+ });
+ break;
+ }
+ }, false);
+ },
+ onCloseWindow: function(aWindow){ },
+ onWindowTitleChange: function(){ },
+};
+
+function startup(aData, aReason) {
+ let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
+ getService (Components.interfaces.nsIWindowMediator);
+
+ Components.manager.addBootstrappedManifestLocation(aData.installPath);
+
+ // Load into any new windows
+ wm.addListener(windowListener);
+}
+
+function shutdown(aData, aReason) {
+ // When the application is shutting down we normally don't have to clean up any UI changes
+ if (aReason == APP_SHUTDOWN)
+ return;
+
+ let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
+ getService(Components.interfaces.nsIWindowMediator);
+
+ // Stop watching for new windows
+ wm.removeListener(windowListener);
+
+ // Unload from any existing windows
+ let enumerator = wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext().QueryInterface(Components.interfaces.nsIDOMWindow);
+ unloadFromWindow(win);
+ }
+}
+
+function install(aData, aReason) { }
+function uninstall(aData, aReason) { }
+
diff --git a/layout/tools/reftest/clean-reftest-output.pl b/layout/tools/reftest/clean-reftest-output.pl
new file mode 100644
index 000000000..b1959281d
--- /dev/null
+++ b/layout/tools/reftest/clean-reftest-output.pl
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+# vim: set shiftwidth=4 tabstop=8 autoindent expandtab:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This script is intended to be run over the standard output of a
+# reftest run. It will extract the parts of the output run relevant to
+# reftest and HTML-ize the URLs.
+
+use strict;
+
+print <<EOM
+<html>
+<head>
+<title>reftest output</title>
+</head>
+<body>
+<pre>
+EOM
+;
+
+while (<>) {
+ next unless /REFTEST/;
+ chomp;
+ chop if /\r$/;
+ s,(TEST-)([^\|]*) \| ([^\|]*) \|(.*),\1\2: <a href="\3">\3</a>\4,;
+ s,(IMAGE[^:]*): (data:.*),<a href="\2">\1</a>,;
+ print;
+ print "\n";
+}
+
+print <<EOM
+</pre>
+</body>
+</html>
+EOM
+;
diff --git a/layout/tools/reftest/install.rdf b/layout/tools/reftest/install.rdf
new file mode 100644
index 000000000..6700e0ad4
--- /dev/null
+++ b/layout/tools/reftest/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>reftest@mozilla.org</em:id>
+#ifdef BOOTSTRAP
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+#endif
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+#expand <em:minVersion>__MOZILLA_VERSION_U__</em:minVersion>
+#expand <em:maxVersion>__MOZILLA_VERSION_U__</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <!-- Front End MetaData -->
+ <em:name>Reftest</em:name>
+ <em:description>Run layout comparison tests.</em:description>
+ <em:creator>L. David Baron</em:creator>
+ </Description>
+</RDF>
diff --git a/layout/tools/reftest/jar.mn b/layout/tools/reftest/jar.mn
new file mode 100644
index 000000000..e9d89b4ea
--- /dev/null
+++ b/layout/tools/reftest/jar.mn
@@ -0,0 +1,14 @@
+reftest.jar:
+% content reftest %content/
+* content/reftest-content.js (reftest-content.js)
+#ifdef BOOTSTRAP
+* content/reftest.jsm (reftest.js)
+#else
+* content/reftest.js (reftest.js)
+ content/reftest.xul (reftest.xul)
+#ifdef XPI_NAME
+% component {32530271-8c1b-4b7d-a812-218e42c6bb23} components/reftest-cmdline.js
+% contract @mozilla.org/commandlinehandler/general-startup;1?type=reftest {32530271-8c1b-4b7d-a812-218e42c6bb23}
+% category command-line-handler m-reftest @mozilla.org/commandlinehandler/general-startup;1?type=reftest
+#endif
+#endif
diff --git a/layout/tools/reftest/mach_commands.py b/layout/tools/reftest/mach_commands.py
new file mode 100644
index 000000000..a7ba5f602
--- /dev/null
+++ b/layout/tools/reftest/mach_commands.py
@@ -0,0 +1,159 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import unicode_literals
+
+import mozpack.path
+import os
+import re
+
+from mozbuild.base import (
+ MachCommandBase,
+ MozbuildObject,
+)
+
+from mach.decorators import (
+ CommandArgument,
+ CommandProvider,
+ Command,
+)
+
+
+DEBUGGER_HELP = 'Debugger binary to run test in. Program name or path.'
+
+
+class ReftestRunner(MozbuildObject):
+ """Easily run reftests.
+
+ This currently contains just the basics for running reftests. We may want
+ to hook up result parsing, etc.
+ """
+
+ def _manifest_file(self, suite):
+ """Returns the manifest file used for a given test suite."""
+ files = {
+ 'reftest': 'reftest.list',
+ 'reftest-ipc': 'reftest.list',
+ 'crashtest': 'crashtests.list',
+ 'crashtest-ipc': 'crashtests.list',
+ }
+ assert suite in files
+ return files[suite]
+
+ def _find_manifest(self, suite, test_file):
+ assert test_file
+ path_arg = self._wrap_path_argument(test_file)
+ relpath = path_arg.relpath()
+
+ if os.path.isdir(path_arg.srcdir_path()):
+ return mozpack.path.join(relpath, self._manifest_file(suite))
+
+ if relpath.endswith('.list'):
+ return relpath
+
+ raise Exception('Running a single test is not currently supported')
+
+ def _make_shell_string(self, s):
+ return "'%s'" % re.sub("'", r"'\''", s)
+
+ def run_reftest_test(self, test_file=None, filter=None, suite=None,
+ debugger=None):
+ """Runs a reftest.
+
+ test_file is a path to a test file. It can be a relative path from the
+ top source directory, an absolute filename, or a directory containing
+ test files.
+
+ filter is a regular expression (in JS syntax, as could be passed to the
+ RegExp constructor) to select which reftests to run from the manifest.
+
+ suite is the type of reftest to run. It can be one of ('reftest',
+ 'crashtest').
+
+ debugger is the program name (in $PATH) or the full path of the
+ debugger to run.
+ """
+
+ if suite not in ('reftest', 'reftest-ipc', 'crashtest', 'crashtest-ipc'):
+ raise Exception('None or unrecognized reftest suite type.')
+
+ env = {}
+ extra_args = []
+
+ if test_file:
+ path = self._find_manifest(suite, test_file)
+ if not os.path.exists(mozpack.path.join(self.topsrcdir, path)):
+ raise Exception('No manifest file was found at %s.' % path)
+ env[b'TEST_PATH'] = path
+ if filter:
+ extra_args.extend(['--filter', self._make_shell_string(filter)])
+
+ pass_thru = False
+
+ if debugger:
+ extra_args.append('--debugger=%s' % debugger)
+ pass_thru = True
+
+ if extra_args:
+ args = [os.environ.get(b'EXTRA_TEST_ARGS', '')]
+ args.extend(extra_args)
+ env[b'EXTRA_TEST_ARGS'] = ' '.join(args)
+
+ # TODO hook up harness via native Python
+ return self._run_make(directory='.', target=suite, append_env=env,
+ pass_thru=pass_thru, ensure_exit_code=False)
+
+
+def ReftestCommand(func):
+ """Decorator that adds shared command arguments to reftest commands."""
+
+ debugger = CommandArgument('--debugger', metavar='DEBUGGER',
+ help=DEBUGGER_HELP)
+ func = debugger(func)
+
+ flter = CommandArgument('--filter', metavar='REGEX',
+ help='A JS regular expression to match test URLs against, to select '
+ 'a subset of tests to run.')
+ func = flter(func)
+
+ path = CommandArgument('test_file', nargs='?', metavar='MANIFEST',
+ help='Reftest manifest file, or a directory in which to select '
+ 'reftest.list. If omitted, the entire test suite is executed.')
+ func = path(func)
+
+ return func
+
+
+@CommandProvider
+class MachCommands(MachCommandBase):
+ @Command('reftest', category='testing', description='Run reftests.')
+ @ReftestCommand
+ def run_reftest(self, test_file, **kwargs):
+ return self._run_reftest(test_file, suite='reftest', **kwargs)
+
+ @Command('reftest-ipc', category='testing',
+ description='Run IPC reftests.')
+ @ReftestCommand
+ def run_ipc(self, test_file, **kwargs):
+ return self._run_reftest(test_file, suite='reftest-ipc', **kwargs)
+
+ @Command('crashtest', category='testing',
+ description='Run crashtests.')
+ @ReftestCommand
+ def run_crashtest(self, test_file, **kwargs):
+ return self._run_reftest(test_file, suite='crashtest', **kwargs)
+
+ @Command('crashtest-ipc', category='testing',
+ description='Run IPC crashtests.')
+ @ReftestCommand
+ def run_crashtest_ipc(self, test_file, **kwargs):
+ return self._run_reftest(test_file, suite='crashtest-ipc', **kwargs)
+
+ def _run_reftest(self, test_file=None, filter=None, suite=None,
+ debugger=None):
+ reftest = self._spawn(ReftestRunner)
+ return reftest.run_reftest_test(test_file, filter=filter, suite=suite,
+ debugger=debugger)
+
+
diff --git a/layout/tools/reftest/moz.build b/layout/tools/reftest/moz.build
new file mode 100644
index 000000000..de8e39932
--- /dev/null
+++ b/layout/tools/reftest/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MODULE = 'reftest'
+
diff --git a/layout/tools/reftest/print-manifest-dirs.py b/layout/tools/reftest/print-manifest-dirs.py
new file mode 100644
index 000000000..76d95f822
--- /dev/null
+++ b/layout/tools/reftest/print-manifest-dirs.py
@@ -0,0 +1,79 @@
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys, os.path, re
+
+commentRE = re.compile(r"\s+#")
+conditionsRE = re.compile(r"^(fails|needs-focus|random|skip|asserts|slow|require-or|silentfail|pref|test-pref|ref-pref|fuzzy)")
+httpRE = re.compile(r"HTTP\((\.\.(\/\.\.)*)\)")
+protocolRE = re.compile(r"^\w+:")
+
+def parseManifest(manifest, dirs):
+ """Parse the reftest manifest |manifest|, adding all directories containing
+ tests (and the dirs containing the manifests themselves) to the set |dirs|."""
+ manifestdir = os.path.dirname(os.path.abspath(manifest))
+ dirs.add(manifestdir)
+ f = file(manifest)
+ urlprefix = ''
+ for line in f:
+ if line[0] == '#':
+ continue # entire line was a comment
+ m = commentRE.search(line)
+ if m:
+ line = line[:m.start()]
+ line = line.strip()
+ if not line:
+ continue
+ items = line.split()
+ while conditionsRE.match(items[0]):
+ del items[0]
+ if items[0] == "HTTP":
+ del items[0]
+ m = httpRE.match(items[0])
+ if m:
+ # need to package the dir referenced here
+ d = os.path.normpath(os.path.join(manifestdir, m.group(1)))
+ dirs.add(d)
+ del items[0]
+
+ if items[0] == "url-prefix":
+ urlprefix = items[1]
+ continue
+ elif items[0] == "default-preferences":
+ continue
+ elif items[0] == "include":
+ parseManifest(os.path.join(manifestdir, items[1]), dirs)
+ continue
+ elif items[0] == "load" or items[0] == "script":
+ testURLs = [items[1]]
+ elif items[0] == "==" or items[0] == "!=":
+ testURLs = items[1:3]
+ for u in testURLs:
+ m = protocolRE.match(u)
+ if m:
+ # can't very well package about: or data: URIs
+ continue
+ d = os.path.dirname(os.path.normpath(os.path.join(manifestdir, urlprefix + u)))
+ dirs.add(d)
+ f.close()
+
+def printTestDirs(topsrcdir, topmanifests):
+ """Parse |topmanifests| and print a list of directories containing the tests
+ within (and the manifests including those tests), relative to |topsrcdir|."""
+ topsrcdir = os.path.abspath(topsrcdir)
+ dirs = set()
+ for manifest in topmanifests:
+ parseManifest(manifest, dirs)
+ for dir in sorted(dirs):
+ d = dir[len(topsrcdir):].replace('\\','/')
+ if d[0] == '/':
+ d = d[1:]
+ print d
+
+if __name__ == '__main__':
+ if len(sys.argv) < 3:
+ print >>sys.stderr, "Usage: %s topsrcdir reftest.list [reftest.list]*" % sys.argv[0]
+ sys.exit(1)
+ printTestDirs(sys.argv[1], sys.argv[2:])
diff --git a/layout/tools/reftest/reftest-analyzer.xhtml b/layout/tools/reftest/reftest-analyzer.xhtml
new file mode 100644
index 000000000..1b1c81d98
--- /dev/null
+++ b/layout/tools/reftest/reftest-analyzer.xhtml
@@ -0,0 +1,579 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- -*- Mode: HTML; tab-width: 4; indent-tabs-mode: nil; -*- -->
+<!-- vim: set shiftwidth=4 tabstop=4 autoindent noexpandtab: -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!--
+
+Features to add:
+* make the left and right parts of the viewer independently scrollable
+* make the test list filterable
+** default to only showing unexpecteds
+* add other ways to highlight differences other than circling?
+* add zoom/pan to images
+* Add ability to load log via XMLHttpRequest (also triggered via URL param)
+* color the test list based on pass/fail and expected/unexpected/random/skip
+* ability to load multiple logs ?
+** rename them by clicking on the name and editing
+** turn the test list into a collapsing tree view
+** move log loading into popup from viewer UI
+
+-->
+<!DOCTYPE html>
+<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Reftest analyzer</title>
+ <style type="text/css"><![CDATA[
+
+ html, body { margin: 0; }
+ html { padding: 0; }
+ body { padding: 4px; }
+
+ #pixelarea, #itemlist, #images { position: absolute; }
+ #itemlist, #images { overflow: auto; }
+ #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
+ #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
+ #images { top: 0; bottom: 0; left: 320px; right: 0; }
+
+ #leftpane { width: 320px; }
+ #images { position: fixed; top: 10px; left: 340px; }
+
+ form#imgcontrols { margin: 0; display: block; }
+
+ #itemlist > table { border-collapse: collapse; }
+ #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
+
+ /*
+ #itemlist > table > tbody > tr.pass > td.url { background: lime; }
+ #itemlist > table > tbody > tr.fail > td.url { background: red; }
+ */
+
+ #magnification > svg { display: block; width: 84px; height: 84px; }
+
+ #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
+ #pixelinfo table { border-collapse: collapse; }
+ #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
+ #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
+
+ #pixelhint { display: inline; color: #88f; cursor: help; }
+ #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
+ #pixelhint:hover { color: #000; }
+ #pixelhint:hover > * { display: block; }
+ #pixelhint p { margin: 0; }
+ #pixelhint p + p { margin-top: 1em; }
+
+ ]]></style>
+ <script type="text/javascript"><![CDATA[
+
+var XLINK_NS = "http://www.w3.org/1999/xlink";
+var SVG_NS = "http://www.w3.org/2000/svg";
+var IMAGE_NOT_AVAILABLE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAASCAYAAADczdVTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHy0lEQVRoge2aX2hb5xnGf2dYabROgqQkpMuKnWUJLmxHMFaa/SscteQiF5EvUgqLctEVrDJKK1+MolzkQr4IctgW+SLIheJc1BpFpswJw92FbaZsTCGTL0465AtntUekJdJ8lByVHbnnwLsLKbKdSJbiZBVjeuAYn+/P+z3fc97vfd9zbEVEhB566BK+1m0CPfx/o+eAPXQVbR3QqVapOl8FlR46h0O1Wu02iacCZfsasMKEz8vbx1JYE6fY/dXx6mEbFObPcvDVDBlznpc9G+2r8xNcvLqK2w39r4UI+fs7tFjmytgFFu718865EIebPGincI3zFz7Bcrtx97/GL0P+p+IPbSOgRwXtW3vpewqL/a/g5rgf39hit2m0hGUAHOHrrq3trmef4/lDB7Ay57n01zuPZXPX7jUunv+Yf9ktR7D/0CHca7/n3KXPsHbAuynkCWCZptgiImKLaVqP9NuW1bT9ceybpr3j+WJbYrVa3rbEatGZi2uixvWdrysilmWKae2M+5PqlktoosayLfubcrN10dAk24aynUsIxMVsadwUs+EX7dEyAlaXLqMoCj6fj5HkUqO9MD+Govjx+xXcXi+uoRAhvwuv182Z8Ws4AJUlxoZ8uNxuvF43ii/EtdXNNUuV68lR/IqC4gsxPj7KkE/BF5qmClRXrzFSt+/1ulDOjLNU6eQ4OcyPDqH4hhg5O4LicuN2K4xcvk6jjHUKJM8O1fvcKMoZkouFOq1VPp1OcuXGAvrvfsv0lWmSySTzN0sdH+jyYhK/ouB2e/G6XfjPJikBVG8SUhT8fl99nwVGfQp+vx+f4iO5VO1AtwJjfgXF58M/kqSVJP9ef0xuAI6NlwWmL41xxqeg+PyMXr72yBqW3cI4JaZHh1DcXrxeLy5liORiB7q1PiZFyeV0mQqz9TRZeUmFVUGLSjqdkgCIFp2RTCosEJOiiIihSyKWkDl9WYrFnCQCCNF0w0QmHhBQJTEzJ+nZSQmAoEYks2KIGBkJgASiM5I3LbGMnCSCCEQl38GJMvMZiag1e+nlFcmmIgKaZEwREaPGhWGZ1VfEMFZkNj4sgCSyhoihSzwSlqCGoAUlEo1IJByW+Oxyh+dZJJ+eklhiRnIrRcnrM6KCxLOmiNiipyICSGR2pTY2O1m7T2XEsNrrJmJLfjkn6amwoMbFaMEhG28eAVtzExErW3sOBCWVzkpmNiEqCOEZ2RyLTT3eJAKaMhVEUMOSXjHEtg3JTIUFkNTK9rGwbQrWm2xGb6QoWxIqEtdtEWO28aDtoi6JSFCAjUtL1AUzJA4SSW/IZ2VjjU0V0zEBJBiJSzwWk1g8IZEAAmrdidrBkoSKxB4IW08tGVNEzIxoIJM5a8v4SQ1RY5lGSy6x8xScz6QkHFBre1Zre49nH+y1KDEQLV7TcyU1LBCtHVppp9smxk2dYAMtHXA7blZWNJDZ4sZ4MxPbdHjrbc3WNuvOq4YlkYhLLBaXeKx2sLcrBUS2ScFtUbUBh3WgajvgOYgGuKjw4Rsqb1uvkssbWLbJXFQFqL/I9IEKa2WzYcqy16E2BNteB1R+cuwoRwcHGRx4nlfenWMuPclRDx3goSraqd+7Gj/Y5d76SrXLu3VKLYW1rMZbo/QpB4+9zt6fT1I0Law/LRMBaLzC7ePNuSgL7/2GpcotLr7+AZG5t9gH0Fa3zuFq1tiWG4DKs5tebV1NDDW1XYd26iWO9A8wODjAUfUN5ubm+Ch4ZFuuLRzQoVwqUCqXyN9fg3tFSuUShVIZhyr5O2vo94o42DwD/PP23fq8Bf5urLO+BoHBwxzc20c++wcmz+lAkWLFATwcf3+YDwIDhMYmuDw+wt5j5+C5ZwDYP/gSoLP6xX5+fOIkJ47/lIP8g49/Nc3tDj59OZUiRR3uFYsAVO/eZoE1yvkyeA6gAaff+zU3SxUcp8LilQucnoFTP3hhix19/garlQqFW9eZOBti9Mqt9mubXwBw+NALeDC4cfVDzgP3i3keUN/nf4uo+hEver/DRaK84/9mY/72uoFTKVMolVn5/HPgPvlSmVKhRL2bSrlEqVyidH8N/d7t2u/lakfcKneLgM4rvxhncbXA6tI8kTffB+0NjnrAqZYplcrk83ceXdtzgB+psHD7S/pfPs7JkydQB1x8dnWS2SVje9GaxkVLl+DmNNC4NJn/S6JxH5nJyNRwrW7Qi7oMgxBMyd9molvmRKO1cExgshG6l9NTEhkOynAkLlOJoKBuhPV8ZlK0h9aNTqVbv3ltEK/VIiAQEN0yZVLbuM+aImLoEgts3VdsJrfFil1M1/ZSv9RAROaWO8n/hkyF1Q3bgeFGygvPrDRG5Wcf1IJbq9rlNrrNbra96aqlUVMSWrNnNiw5uw23T/4o4Xq7FtA29h2My3K9WtETgRZr13UxdIk+pGswkpCcsX0N2OZD9BOgWqFsgWePp20KWb0ywkDgEIa8y55Gq0O5XKHP7cGz++l/haxWylgOuD17aG7eoVpxwL27RX8b27jZ42n1qdahXKrg2bfnUW0eQ7edoD232l+/LPp2pHvNfh8eT2f8/3sO2AZLyRAvns6gqToLOgxP6Uz87HvdoNJDF9E1B6ysLrLw5yW+3PUNvv3dH/L9wX3doNFDl9E1B+yhB+j9O1YPXcZ/AAl9BWJNvZE7AAAAAElFTkSuQmCC";
+
+var gPhases = null;
+
+var gIDCache = {};
+
+var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
+var gMagWidth = 5; // number of zoomed in pixels to show horizontally
+var gMagHeight = 5; // number of zoomed in pixels to show vertically
+var gMagZoom = 16; // size of the zoomed in pixels
+var gImage1Data; // ImageData object for the reference image
+var gImage2Data; // ImageData object for the test output image
+var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
+
+function ID(id) {
+ if (!(id in gIDCache))
+ gIDCache[id] = document.getElementById(id);
+ return gIDCache[id];
+}
+
+function hash_parameters() {
+ var result = { };
+ var params = window.location.hash.substr(1).split(/[&;]/);
+ for (var i = 0; i < params.length; i++) {
+ var parts = params[i].split("=");
+ result[parts[0]] = unescape(unescape(parts[1]));
+ }
+ return result;
+}
+
+function load() {
+ gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
+ build_mag();
+ var params = hash_parameters();
+ if (params.log) {
+ ID("logentry").value = params.log;
+ log_pasted();
+ }
+ window.addEventListener('keypress', maybe_load_image, false);
+ ID("image1").addEventListener('error', image_load_error, false);
+ ID("image2").addEventListener('error', image_load_error, false);
+}
+
+function image_load_error(e) {
+ e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
+}
+
+function build_mag() {
+ var mag = ID("mag");
+
+ var r = document.createElementNS(SVG_NS, "rect");
+ r.setAttribute("x", gMagZoom * -gMagWidth / 2);
+ r.setAttribute("y", gMagZoom * -gMagHeight / 2);
+ r.setAttribute("width", gMagZoom * gMagWidth);
+ r.setAttribute("height", gMagZoom * gMagHeight);
+ mag.appendChild(r);
+
+ mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
+
+ for (var x = 0; x < gMagWidth; x++) {
+ gMagPixPaths[x] = [];
+ for (var y = 0; y < gMagHeight; y++) {
+ var p1 = document.createElementNS(SVG_NS, "path");
+ p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
+ p1.setAttribute("stroke", "black");
+ p1.setAttribute("stroke-width", "1px");
+ p1.setAttribute("fill", "#aaa");
+
+ var p2 = document.createElementNS(SVG_NS, "path");
+ p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
+ p2.setAttribute("stroke", "black");
+ p2.setAttribute("stroke-width", "1px");
+ p2.setAttribute("fill", "#888");
+
+ mag.appendChild(p1);
+ mag.appendChild(p2);
+ gMagPixPaths[x][y] = [p1, p2];
+ }
+ }
+
+ var flashedOn = false;
+ setInterval(function() {
+ flashedOn = !flashedOn;
+ flash_pixels(flashedOn);
+ }, 500);
+}
+
+function show_phase(phaseid) {
+ for (var i in gPhases) {
+ var phase = gPhases[i];
+ phase.style.display = (phase.id == phaseid) ? "" : "none";
+ }
+
+ if (phase == "viewer")
+ ID("images").style.display = "none";
+}
+
+function fileentry_changed() {
+ show_phase("loading");
+ var input = ID("fileentry");
+ var files = input.files;
+ if (files.length > 0) {
+ // Only handle the first file; don't handle multiple selection.
+ // The parts of the log we care about are ASCII-only. Since we
+ // can ignore lines we don't care about, best to read in as
+ // iso-8859-1, which guarantees we don't get decoding errors.
+ var fileReader = new FileReader();
+ fileReader.onload = function(e) {
+ var log = null;
+
+ log = e.target.result;
+
+ if (log)
+ process_log(log);
+ else
+ show_phase("entry");
+ }
+ fileReader.readAsText(files[0], "iso-8859-1");
+ }
+ // So the user can process the same filename again (after
+ // overwriting the log), clear the value on the form input so we
+ // will always get an onchange event.
+ input.value = "";
+}
+
+function log_pasted() {
+ show_phase("loading");
+ var entry = ID("logentry");
+ var log = entry.value;
+ entry.value = "";
+ process_log(log);
+}
+
+var gTestItems;
+
+function process_log(contents) {
+ var lines = contents.split(/[\r\n]+/);
+ gTestItems = [];
+ for (var j in lines) {
+ var line = lines[j];
+ var match = line.match(/^(?:NEXT ERROR |\d\d:\d\d:\d\d +INFO - +)*REFTEST (.*)$/);
+ if (!match)
+ continue;
+ line = match[1];
+ match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
+ if (match) {
+ var state = match[1];
+ var random = match[2];
+ var url = match[3];
+ var extra = match[4];
+ gTestItems.push(
+ {
+ pass: !state.match(/DEBUG-INFO$|FAIL$/),
+ // only one of the following three should ever be true
+ unexpected: !!state.match(/^TEST-UNEXPECTED/),
+ random: (random == "(EXPECTED RANDOM)"),
+ skip: (extra == " (SKIP)"),
+ url: url,
+ images: []
+ });
+ continue;
+ }
+ match = line.match(/^ IMAGE[^:]*: (.*)$/);
+ if (match) {
+ var item = gTestItems[gTestItems.length - 1];
+ item.images.push(match[1]);
+ }
+ }
+
+ build_viewer();
+}
+
+function build_viewer() {
+ if (gTestItems.length == 0) {
+ show_phase("entry");
+ return;
+ }
+
+ var cell = ID("itemlist");
+ while (cell.childNodes.length > 0)
+ cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
+
+ var table = document.createElement("table");
+ var tbody = document.createElement("tbody");
+ table.appendChild(tbody);
+
+ for (var i in gTestItems) {
+ var item = gTestItems[i];
+
+ // XXX skip expected pass items until we have filtering UI
+ if (item.pass && !item.unexpected)
+ continue;
+
+ var tr = document.createElement("tr");
+ var rowclass = item.pass ? "pass" : "fail";
+ var td;
+ var text;
+
+ td = document.createElement("td");
+ text = "";
+ if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
+ if (item.random) { text += "R"; rowclass += " random"; }
+ if (item.skip) { text += "S"; rowclass += " skip"; }
+ td.appendChild(document.createTextNode(text));
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ td.className = "url";
+ // Only display part of URL after "/mozilla/".
+ var match = item.url.match(/\/mozilla\/(.*)/);
+ text = document.createTextNode(match ? match[1] : item.url);
+ if (item.images.length > 0) {
+ var a = document.createElement("a");
+ a.href = "javascript:show_images(" + i + ")";
+ a.appendChild(text);
+ td.appendChild(a);
+ } else {
+ td.appendChild(text);
+ }
+ tr.appendChild(td);
+
+ tbody.appendChild(tr);
+ }
+
+ cell.appendChild(table);
+
+ show_phase("viewer");
+}
+
+function get_image_data(src, whenReady) {
+ var img = new Image();
+ img.onload = function() {
+ var canvas = document.createElement("canvas");
+ canvas.width = 800;
+ canvas.height = 1000;
+
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0);
+
+ whenReady(ctx.getImageData(0, 0, 800, 1000));
+ };
+ img.src = src;
+}
+
+function show_images(i) {
+ var item = gTestItems[i];
+ var cell = ID("images");
+
+ ID("image1").style.display = "";
+ ID("image2").style.display = "none";
+ ID("diffrect").style.display = "none";
+ ID("imgcontrols").reset();
+
+ ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+ // Making the href be #image1 doesn't seem to work
+ ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+ if (item.images.length == 1) {
+ ID("imgcontrols").style.display = "none";
+ } else {
+ ID("imgcontrols").style.display = "";
+
+ ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+ // Making the href be #image2 doesn't seem to work
+ ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+ }
+
+ cell.style.display = "";
+
+ get_image_data(item.images[0], function(data) { gImage1Data = data });
+ get_image_data(item.images[1], function(data) { gImage2Data = data });
+}
+
+function show_image(i) {
+ if (i == 1) {
+ ID("image1").style.display = "";
+ ID("image2").style.display = "none";
+ } else {
+ ID("image1").style.display = "none";
+ ID("image2").style.display = "";
+ }
+}
+
+function maybe_load_image(event) {
+ switch (event.charCode) {
+ case 49: // "1" key
+ document.getElementById("radio1").checked = true;
+ show_image(1);
+ break;
+ case 50: // "2" key
+ document.getElementById("radio2").checked = true;
+ show_image(2);
+ break;
+ }
+}
+
+function show_differences(cb) {
+ ID("diffrect").style.display = cb.checked ? "" : "none";
+}
+
+function flash_pixels(on) {
+ var stroke = on ? "red" : "black";
+ var strokeWidth = on ? "2px" : "1px";
+ for (var i = 0; i < gFlashingPixels.length; i++) {
+ gFlashingPixels[i].setAttribute("stroke", stroke);
+ gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
+ }
+}
+
+function cursor_point(evt) {
+ var m = evt.target.getScreenCTM().inverse();
+ var p = ID("svg").createSVGPoint();
+ p.x = evt.clientX;
+ p.y = evt.clientY;
+ p = p.matrixTransform(m);
+ return { x: Math.floor(p.x), y: Math.floor(p.y) };
+}
+
+function hex2(i) {
+ return (i < 16 ? "0" : "") + i.toString(16);
+}
+
+function canvas_pixel_as_hex(data, x, y) {
+ var offset = (y * data.width + x) * 4;
+ var r = data.data[offset];
+ var g = data.data[offset + 1];
+ var b = data.data[offset + 2];
+ return "#" + hex2(r) + hex2(g) + hex2(b);
+}
+
+function hex_as_rgb(hex) {
+ return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
+}
+
+function magnify(evt) {
+ var { x: x, y: y } = cursor_point(evt);
+ var centerPixelColor1, centerPixelColor2;
+
+ var dx_lo = -Math.floor(gMagWidth / 2);
+ var dx_hi = Math.floor(gMagWidth / 2);
+ var dy_lo = -Math.floor(gMagHeight / 2);
+ var dy_hi = Math.floor(gMagHeight / 2);
+
+ flash_pixels(false);
+ gFlashingPixels = [];
+ for (var j = dy_lo; j <= dy_hi; j++) {
+ for (var i = dx_lo; i <= dx_hi; i++) {
+ var px = x + i;
+ var py = y + j;
+ var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
+ var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
+ if (px < 0 || py < 0 || px >= 800 || py >= 1000) {
+ p1.setAttribute("fill", "#aaa");
+ p2.setAttribute("fill", "#888");
+ } else {
+ var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
+ var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
+ p1.setAttribute("fill", color1);
+ p2.setAttribute("fill", color2);
+ if (color1 != color2) {
+ gFlashingPixels.push(p1, p2);
+ p1.parentNode.appendChild(p1);
+ p2.parentNode.appendChild(p2);
+ }
+ if (i == 0 && j == 0) {
+ centerPixelColor1 = color1;
+ centerPixelColor2 = color2;
+ }
+ }
+ }
+ }
+ flash_pixels(true);
+ show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
+}
+
+function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
+ var pixelinfo = ID("pixelinfo");
+ ID("coords").textContent = [x, y];
+ ID("pix1hex").textContent = pix1hex;
+ ID("pix1rgb").textContent = pix1rgb;
+ ID("pix2hex").textContent = pix2hex;
+ ID("pix2rgb").textContent = pix2rgb;
+}
+
+ ]]></script>
+
+</head>
+<body onload="load()">
+
+<div id="entry">
+
+<h1>Reftest analyzer: load reftest log</h1>
+
+<p>Either paste your log into this textarea:<br />
+<textarea cols="80" rows="10" id="logentry"/><br/>
+<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
+
+<p>... or load it from a file:<br/>
+<input type="file" id="fileentry" onchange="fileentry_changed()" />
+</p>
+</div>
+
+<div id="loading" style="display:none">Loading log...</div>
+
+<div id="viewer" style="display:none">
+ <div id="pixelarea">
+ <div id="pixelinfo">
+ <table>
+ <tbody>
+ <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
+ <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
+ <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
+ </tbody>
+ </table>
+ <div>
+ <div id="pixelhint">★
+ <div>
+ <p>Move the mouse over the reftest image on the right to show
+ magnified pixels on the left. The color information above is for
+ the pixel centered in the magnified view.</p>
+ <p>Image 1 is shown in the upper triangle of each pixel and Image 2
+ is shown in the lower triangle.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="magnification">
+ <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
+ <g id="mag"/>
+ </svg>
+ </div>
+ </div>
+ <div id="itemlist"></div>
+ <div id="images" style="display:none">
+ <form id="imgcontrols">
+ <label title="1"><input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" />Image 1</label>
+ <label title="2"><input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" />Image 2</label>
+ <label><input type="checkbox" onchange="show_differences(this)" />Circle differences</label>
+ </form>
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="1000px" viewBox="0 0 800 1000" id="svg">
+ <defs>
+ <!-- use sRGB to avoid loss of data -->
+ <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
+ style="color-interpolation-filters: sRGB">
+ <feImage id="feimage1" result="img1" xlink:href="#image1" />
+ <feImage id="feimage2" result="img2" xlink:href="#image2" />
+ <!-- inv1 and inv2 are the images with RGB inverted -->
+ <feComponentTransfer result="inv1" in="img1">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <feComponentTransfer result="inv2" in="img2">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <!-- w1 will have non-white pixels anywhere that img2
+ is brighter than img1, and w2 for the reverse.
+ It would be nice not to have to go through these
+ intermediate states, but feComposite
+ type="arithmetic" can't transform the RGB channels
+ and leave the alpha channel untouched. -->
+ <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
+ <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
+ <!-- c1 will have non-black pixels anywhere that img2
+ is brighter than img1, and c2 for the reverse -->
+ <feComponentTransfer result="c1" in="w1">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <feComponentTransfer result="c2" in="w2">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
+ <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
+ <!-- a will be opaque for every pixel with differences and transparent for all others -->
+ <feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
+
+ <!-- a, dilated by 1 pixel -->
+ <feMorphology result="dila1" in="a" operator="dilate" radius="1" />
+ <!-- a, dilated by 2 pixels -->
+ <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
+
+ <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
+ <feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
+
+ <feFlood result="red" flood-color="red" />
+ <feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
+ <feFlood result="black" flood-color="black" flood-opacity="0.5" />
+ <feMerge>
+ <feMergeNode in="black" />
+ <feMergeNode in="redhighlight" />
+ </feMerge>
+ </filter>
+ </defs>
+ <g onmousemove="magnify(evt)">
+ <image x="0" y="0" width="100%" height="100%" id="image1" />
+ <image x="0" y="0" width="100%" height="100%" id="image2" />
+ </g>
+ <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
+ </svg>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/layout/tools/reftest/reftest-cmdline.js b/layout/tools/reftest/reftest-cmdline.js
new file mode 100644
index 000000000..107856151
--- /dev/null
+++ b/layout/tools/reftest/reftest-cmdline.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const nsISupports = Components.interfaces.nsISupports;
+
+const nsICommandLine = Components.interfaces.nsICommandLine;
+const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
+const nsISupportsString = Components.interfaces.nsISupportsString;
+const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
+
+function RefTestCmdLineHandler() {}
+RefTestCmdLineHandler.prototype =
+{
+ classID: Components.ID('{32530271-8c1b-4b7d-a812-218e42c6bb23}'),
+
+ /* nsISupports */
+ QueryInterface: XPCOMUtils.generateQI([nsICommandLineHandler]),
+
+ /* nsICommandLineHandler */
+ handle : function handler_handle(cmdLine) {
+ var args = { };
+ args.wrappedJSObject = args;
+ try {
+ var uristr = cmdLine.handleFlagWithParam("reftest", false);
+ if (uristr == null)
+ return;
+ try {
+ args.uri = cmdLine.resolveURI(uristr).spec;
+ }
+ catch (e) {
+ return;
+ }
+ }
+ catch (e) {
+ cmdLine.handleFlag("reftest", true);
+ }
+
+ try {
+ var nocache = cmdLine.handleFlag("reftestnocache", false);
+ args.nocache = nocache;
+ }
+ catch (e) {
+ }
+
+ try {
+ var skipslowtests = cmdLine.handleFlag("reftestskipslowtests", false);
+ args.skipslowtests = skipslowtests;
+ }
+ catch (e) {
+ }
+
+ /* Ignore the platform's online/offline status while running reftests. */
+ var ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService2);
+ ios.manageOfflineStatus = false;
+ ios.offline = false;
+
+ /**
+ * Manipulate preferences by adding to the *default* branch. Adding
+ * to the default branch means the changes we make won't get written
+ * back to user preferences.
+ *
+ * We want to do this here rather than in reftest.js because it's
+ * important to force sRGB as an output profile for color management
+ * before we load a window.
+ *
+ * If you change these, please adjust them in the bootstrap.js function
+ * setDefaultPrefs(). These are duplicated there so we can have a
+ * restartless addon for reftest on native Android.
+ *
+ * FIXME: These should be in only one place.
+ */
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefService);
+ var branch = prefs.getDefaultBranch("");
+ // For mochitests, we're more interested in testing the behavior of in-
+ // content XBL bindings, so we set this pref to true. In reftests, we're
+ // more interested in testing the behavior of XBL as it works in chrome,
+ // so we want this pref to be false.
+ branch.setBoolPref("dom.use_xbl_scopes_for_remote_xul", false);
+ branch.setBoolPref("gfx.color_management.force_srgb", true);
+ branch.setBoolPref("browser.dom.window.dump.enabled", true);
+ branch.setIntPref("ui.caretBlinkTime", -1);
+ branch.setBoolPref("dom.send_after_paint_to_content", true);
+ // no slow script dialogs
+ branch.setIntPref("dom.max_script_run_time", 0);
+ branch.setIntPref("dom.max_chrome_script_run_time", 0);
+ branch.setIntPref("hangmonitor.timeout", 0);
+ // Ensure autoplay is enabled for all platforms.
+ branch.setBoolPref("media.autoplay.enabled", true);
+ // Disable updates
+ branch.setBoolPref("app.update.enabled", false);
+ // Disable addon updates and prefetching so we don't leak them
+ branch.setBoolPref("extensions.update.enabled", false);
+ branch.setBoolPref("extensions.getAddons.cache.enabled", false);
+ // Disable blocklist updates so we don't have them reported as leaks
+ branch.setBoolPref("extensions.blocklist.enabled", false);
+ // Make url-classifier updates so rare that they won't affect tests
+ branch.setIntPref("urlclassifier.updateinterval", 172800);
+ // Disable high-quality downscaling, since it makes reftests more difficult.
+ branch.setBoolPref("image.high_quality_downscaling.enabled", false);
+ // Checking whether two files are the same is slow on Windows.
+ // Setting this pref makes tests run much faster there.
+ branch.setBoolPref("security.fileuri.strict_origin_policy", false);
+
+ var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(nsIWindowWatcher);
+ wwatch.openWindow(null, "chrome://reftest/content/reftest.xul", "_blank",
+ "chrome,dialog=no,all", args);
+ cmdLine.preventDefault = true;
+ },
+
+ helpInfo : " -reftest <file> Run layout acceptance tests on given manifest.\n"
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RefTestCmdLineHandler]);
diff --git a/layout/tools/reftest/reftest-cmdline.manifest b/layout/tools/reftest/reftest-cmdline.manifest
new file mode 100644
index 000000000..f046dd886
--- /dev/null
+++ b/layout/tools/reftest/reftest-cmdline.manifest
@@ -0,0 +1,3 @@
+component {32530271-8c1b-4b7d-a812-218e42c6bb23} reftest-cmdline.js
+contract @mozilla.org/commandlinehandler/general-startup;1?type=reftest {32530271-8c1b-4b7d-a812-218e42c6bb23}
+category command-line-handler m-reftest @mozilla.org/commandlinehandler/general-startup;1?type=reftest
diff --git a/layout/tools/reftest/reftest-content.js b/layout/tools/reftest/reftest-content.js
new file mode 100644
index 000000000..09ee3e2c7
--- /dev/null
+++ b/layout/tools/reftest/reftest-content.js
@@ -0,0 +1,896 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- /
+/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CR = Components.results;
+const CU = Components.utils;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const DEBUG_CONTRACTID = "@mozilla.org/xpcom/debug;1";
+const PRINTSETTINGS_CONTRACTID = "@mozilla.org/gfx/printsettings-service;1";
+const ENVIRONMENT_CONTRACTID = "@mozilla.org/process/environment;1";
+
+// "<!--CLEAR-->"
+const BLANK_URL_FOR_CLEARING = "data:text/html;charset=UTF-8,%3C%21%2D%2DCLEAR%2D%2D%3E";
+
+CU.import("resource://gre/modules/Timer.jsm");
+CU.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm");
+
+var gBrowserIsRemote;
+var gHaveCanvasSnapshot = false;
+// Plugin layers can be updated asynchronously, so to make sure that all
+// layer surfaces have the right content, we need to listen for explicit
+// "MozPaintWait" and "MozPaintWaitFinished" events that signal when it's OK
+// to take snapshots. We cannot take a snapshot while the number of
+// "MozPaintWait" events fired exceeds the number of "MozPaintWaitFinished"
+// events fired. We count the number of such excess events here. When
+// the counter reaches zero we call gExplicitPendingPaintsCompleteHook.
+var gExplicitPendingPaintCount = 0;
+var gExplicitPendingPaintsCompleteHook;
+var gCurrentURL;
+var gCurrentTestType;
+var gTimeoutHook = null;
+var gFailureTimeout = null;
+var gFailureReason;
+var gAssertionCount = 0;
+
+var gDebug;
+var gVerbose = false;
+
+var gCurrentTestStartTime;
+var gClearingForAssertionCheck = false;
+
+const TYPE_LOAD = 'load'; // test without a reference (just test that it does
+ // not assert, crash, hang, or leak)
+const TYPE_SCRIPT = 'script'; // test contains individual test results
+
+function markupDocumentViewer() {
+ return docShell.contentViewer.QueryInterface(CI.nsIMarkupDocumentViewer);
+}
+
+function webNavigation() {
+ return docShell.QueryInterface(CI.nsIWebNavigation);
+}
+
+function windowUtils() {
+ return content.QueryInterface(CI.nsIInterfaceRequestor)
+ .getInterface(CI.nsIDOMWindowUtils);
+}
+
+function IDForEventTarget(event)
+{
+ try {
+ return "'" + event.target.getAttribute('id') + "'";
+ } catch (ex) {
+ return "<unknown>";
+ }
+}
+
+function PaintWaitListener(event)
+{
+ LogInfo("MozPaintWait received for ID " + IDForEventTarget(event));
+ gExplicitPendingPaintCount++;
+}
+
+function PaintWaitFinishedListener(event)
+{
+ LogInfo("MozPaintWaitFinished received for ID " + IDForEventTarget(event));
+ gExplicitPendingPaintCount--;
+ if (gExplicitPendingPaintCount < 0) {
+ LogWarning("Underrun in gExplicitPendingPaintCount\n");
+ gExplicitPendingPaintCount = 0;
+ }
+ if (gExplicitPendingPaintCount == 0 &&
+ gExplicitPendingPaintsCompleteHook) {
+ gExplicitPendingPaintsCompleteHook();
+ }
+}
+
+function OnInitialLoad()
+{
+#ifndef REFTEST_B2G
+ removeEventListener("load", OnInitialLoad, true);
+#endif
+
+ gDebug = CC[DEBUG_CONTRACTID].getService(CI.nsIDebug2);
+ var env = CC[ENVIRONMENT_CONTRACTID].getService(CI.nsIEnvironment);
+ gVerbose = !!env.get("MOZ_REFTEST_VERBOSE");
+
+ RegisterMessageListeners();
+
+ var initInfo = SendContentReady();
+ gBrowserIsRemote = initInfo.remote;
+
+ addEventListener("load", OnDocumentLoad, true);
+
+ addEventListener("MozPaintWait", PaintWaitListener, true);
+ addEventListener("MozPaintWaitFinished", PaintWaitFinishedListener, true);
+
+ LogWarning("Using browser remote="+ gBrowserIsRemote +"\n");
+}
+
+function StartTestURI(type, uri, timeout)
+{
+ // Reset gExplicitPendingPaintCount in case there was a timeout or
+ // the count is out of sync for some other reason
+ if (gExplicitPendingPaintCount != 0) {
+ LogWarning("Resetting gExplicitPendingPaintCount to zero (currently " +
+ gExplicitPendingPaintCount + "\n");
+ gExplicitPendingPaintCount = 0;
+ }
+
+ gCurrentTestType = type;
+ gCurrentURL = uri;
+
+ gCurrentTestStartTime = Date.now();
+ if (gFailureTimeout != null) {
+ SendException("program error managing timeouts\n");
+ }
+ gFailureTimeout = setTimeout(LoadFailed, timeout);
+
+ LoadURI(gCurrentURL);
+}
+
+function setupZoom(contentRootElement) {
+ if (!contentRootElement || !contentRootElement.hasAttribute('reftest-zoom'))
+ return;
+ markupDocumentViewer().fullZoom =
+ contentRootElement.getAttribute('reftest-zoom');
+}
+
+function resetZoom() {
+ markupDocumentViewer().fullZoom = 1.0;
+}
+
+function doPrintMode(contentRootElement) {
+#if REFTEST_B2G
+ // nsIPrintSettings not available in B2G
+ return false;
+#else
+ // use getAttribute because className works differently in HTML and SVG
+ return contentRootElement &&
+ contentRootElement.hasAttribute('class') &&
+ contentRootElement.getAttribute('class').split(/\s+/)
+ .indexOf("reftest-print") != -1;
+#endif
+}
+
+function setupPrintMode() {
+ var PSSVC =
+ CC[PRINTSETTINGS_CONTRACTID].getService(CI.nsIPrintSettingsService);
+ var ps = PSSVC.newPrintSettings;
+ ps.paperWidth = 5;
+ ps.paperHeight = 3;
+
+ // Override any os-specific unwriteable margins
+ ps.unwriteableMarginTop = 0;
+ ps.unwriteableMarginLeft = 0;
+ ps.unwriteableMarginBottom = 0;
+ ps.unwriteableMarginRight = 0;
+
+ ps.headerStrLeft = "";
+ ps.headerStrCenter = "";
+ ps.headerStrRight = "";
+ ps.footerStrLeft = "";
+ ps.footerStrCenter = "";
+ ps.footerStrRight = "";
+ docShell.contentViewer.setPageMode(true, ps);
+}
+
+function setupDisplayport(contentRootElement) {
+ if (!contentRootElement) {
+ return;
+ }
+
+ function attrOrDefault(attr, def) {
+ return contentRootElement.hasAttribute(attr) ?
+ contentRootElement.getAttribute(attr) : def;
+ }
+
+ var vw = attrOrDefault("reftest-viewport-w", 0);
+ var vh = attrOrDefault("reftest-viewport-h", 0);
+ if (vw !== 0 || vh !== 0) {
+ LogInfo("Setting viewport to <w="+ vw +", h="+ vh +">");
+ windowUtils().setCSSViewport(vw, vh);
+ }
+
+ // XXX support displayPortX/Y when needed
+ var dpw = attrOrDefault("reftest-displayport-w", 0);
+ var dph = attrOrDefault("reftest-displayport-h", 0);
+ var dpx = attrOrDefault("reftest-displayport-x", 0);
+ var dpy = attrOrDefault("reftest-displayport-y", 0);
+ if (dpw !== 0 || dph !== 0) {
+ LogInfo("Setting displayport to <x="+ dpx +", y="+ dpy +", w="+ dpw +", h="+ dph +">");
+ windowUtils().setDisplayPortForElement(dpx, dpy, dpw, dph, content.document.documentElement);
+ }
+ var asyncScroll = attrOrDefault("reftest-async-scroll", false);
+ if (asyncScroll) {
+ SendEnableAsyncScroll();
+ }
+
+ // XXX support resolution when needed
+
+ // XXX support viewconfig when needed
+}
+
+function resetDisplayport() {
+ // XXX currently the displayport configuration lives on the
+ // presshell and so is "reset" on nav when we get a new presshell.
+}
+
+function shouldWaitForExplicitPaintWaiters() {
+ return gExplicitPendingPaintCount > 0;
+}
+
+function shouldWaitForPendingPaints() {
+ // if gHaveCanvasSnapshot is false, we're not taking snapshots so
+ // there is no need to wait for pending paints to be flushed.
+ return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending;
+}
+
+function shouldWaitForReftestWaitRemoval(contentRootElement) {
+ // use getAttribute because className works differently in HTML and SVG
+ return contentRootElement &&
+ contentRootElement.hasAttribute('class') &&
+ contentRootElement.getAttribute('class').split(/\s+/)
+ .indexOf("reftest-wait") != -1;
+}
+
+function shouldSnapshotWholePage(contentRootElement) {
+ // use getAttribute because className works differently in HTML and SVG
+ return contentRootElement &&
+ contentRootElement.hasAttribute('class') &&
+ contentRootElement.getAttribute('class').split(/\s+/)
+ .indexOf("reftest-snapshot-all") != -1;
+}
+
+function getNoPaintElements(contentRootElement) {
+ return contentRootElement.getElementsByClassName('reftest-no-paint');
+}
+
+// Initial state. When the document has loaded and all MozAfterPaint events and
+// all explicit paint waits are flushed, we can fire the MozReftestInvalidate
+// event and move to the next state.
+const STATE_WAITING_TO_FIRE_INVALIDATE_EVENT = 0;
+// When reftest-wait has been removed from the root element, we can move to the
+// next state.
+const STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL = 1;
+// When spell checking is done on all spell-checked elements, we can move to the
+// next state.
+const STATE_WAITING_FOR_SPELL_CHECKS = 2;
+// When all MozAfterPaint events and all explicit paint waits are flushed, we're
+// done and can move to the COMPLETED state.
+const STATE_WAITING_TO_FINISH = 3;
+const STATE_COMPLETED = 4;
+
+function WaitForTestEnd(contentRootElement, inPrintMode, spellCheckedElements) {
+ var stopAfterPaintReceived = false;
+ var currentDoc = content.document;
+ var state = STATE_WAITING_TO_FIRE_INVALIDATE_EVENT;
+
+ function FlushRendering() {
+ var anyPendingPaintsGeneratedInDescendants = false;
+
+ function flushWindow(win) {
+ var utils = win.QueryInterface(CI.nsIInterfaceRequestor)
+ .getInterface(CI.nsIDOMWindowUtils);
+ var afterPaintWasPending = utils.isMozAfterPaintPending;
+
+ try {
+ // Flush pending restyles and reflows for this window
+ win.document.documentElement.getBoundingClientRect();
+ } catch (e) {
+ LogWarning("flushWindow failed: " + e + "\n");
+ }
+
+ if (!afterPaintWasPending && utils.isMozAfterPaintPending) {
+ LogInfo("FlushRendering generated paint for window " + win.location.href);
+ anyPendingPaintsGeneratedInDescendants = true;
+ }
+
+ for (var i = 0; i < win.frames.length; ++i) {
+ flushWindow(win.frames[i]);
+ }
+ }
+
+ flushWindow(content);
+
+ if (anyPendingPaintsGeneratedInDescendants &&
+ !windowUtils().isMozAfterPaintPending) {
+ LogWarning("Internal error: descendant frame generated a MozAfterPaint event, but the root document doesn't have one!");
+ }
+ }
+
+ function AfterPaintListener(event) {
+ LogInfo("AfterPaintListener in " + event.target.document.location.href);
+ if (event.target.document != currentDoc) {
+ // ignore paint events for subframes or old documents in the window.
+ // Invalidation in subframes will cause invalidation in the toplevel document anyway.
+ return;
+ }
+
+ SendUpdateCanvasForEvent(event, contentRootElement);
+ // These events are fired immediately after a paint. Don't
+ // confuse ourselves by firing synchronously if we triggered the
+ // paint ourselves.
+ setTimeout(MakeProgress, 0);
+ }
+
+ function AttrModifiedListener() {
+ LogInfo("AttrModifiedListener fired");
+ // Wait for the next return-to-event-loop before continuing --- for
+ // example, the attribute may have been modified in an subdocument's
+ // load event handler, in which case we need load event processing
+ // to complete and unsuppress painting before we check isMozAfterPaintPending.
+ setTimeout(MakeProgress, 0);
+ }
+
+ function ExplicitPaintsCompleteListener() {
+ LogInfo("ExplicitPaintsCompleteListener fired");
+ // Since this can fire while painting, don't confuse ourselves by
+ // firing synchronously. It's fine to do this asynchronously.
+ setTimeout(MakeProgress, 0);
+ }
+
+ function RemoveListeners() {
+ // OK, we can end the test now.
+ removeEventListener("MozAfterPaint", AfterPaintListener, false);
+ if (contentRootElement) {
+ contentRootElement.removeEventListener("DOMAttrModified", AttrModifiedListener, false);
+ }
+ gExplicitPendingPaintsCompleteHook = null;
+ gTimeoutHook = null;
+ // Make sure we're in the COMPLETED state just in case
+ // (this may be called via the test-timeout hook)
+ state = STATE_COMPLETED;
+ }
+
+ // Everything that could cause shouldWaitForXXX() to
+ // change from returning true to returning false is monitored via some kind
+ // of event listener which eventually calls this function.
+ function MakeProgress() {
+ if (state >= STATE_COMPLETED) {
+ LogInfo("MakeProgress: STATE_COMPLETED");
+ return;
+ }
+
+ FlushRendering();
+
+ switch (state) {
+ case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: {
+ LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT");
+ if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
+ gFailureReason = "timed out waiting for pending paint count to reach zero";
+ if (shouldWaitForExplicitPaintWaiters()) {
+ gFailureReason += " (waiting for MozPaintWaitFinished)";
+ LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
+ }
+ if (shouldWaitForPendingPaints()) {
+ gFailureReason += " (waiting for MozAfterPaint)";
+ LogInfo("MakeProgress: waiting for MozAfterPaint");
+ }
+ return;
+ }
+
+ state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL;
+ var hasReftestWait = shouldWaitForReftestWaitRemoval(contentRootElement);
+ // Notify the test document that now is a good time to test some invalidation
+ LogInfo("MakeProgress: dispatching MozReftestInvalidate");
+ if (contentRootElement) {
+ var elements = getNoPaintElements(contentRootElement);
+ for (var i = 0; i < elements.length; ++i) {
+ windowUtils().checkAndClearPaintedState(elements[i]);
+ }
+ var notification = content.document.createEvent("Events");
+ notification.initEvent("MozReftestInvalidate", true, false);
+ contentRootElement.dispatchEvent(notification);
+ }
+ if (hasReftestWait && !shouldWaitForReftestWaitRemoval(contentRootElement)) {
+ // MozReftestInvalidate handler removed reftest-wait.
+ // We expect something to have been invalidated...
+ FlushRendering();
+ if (!shouldWaitForPendingPaints() && !shouldWaitForExplicitPaintWaiters()) {
+ LogWarning("MozInvalidateEvent didn't invalidate");
+ }
+ }
+ // Try next state
+ MakeProgress();
+ return;
+ }
+
+ case STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL:
+ LogInfo("MakeProgress: STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL");
+ if (shouldWaitForReftestWaitRemoval(contentRootElement)) {
+ gFailureReason = "timed out waiting for reftest-wait to be removed";
+ LogInfo("MakeProgress: waiting for reftest-wait to be removed");
+ return;
+ }
+
+ // Try next state
+ state = STATE_WAITING_FOR_SPELL_CHECKS;
+ MakeProgress();
+ return;
+
+ case STATE_WAITING_FOR_SPELL_CHECKS:
+ LogInfo("MakeProgress: STATE_WAITING_FOR_SPELL_CHECKS");
+ if (numPendingSpellChecks) {
+ gFailureReason = "timed out waiting for spell checks to end";
+ LogInfo("MakeProgress: waiting for spell checks to end");
+ return;
+ }
+
+ state = STATE_WAITING_TO_FINISH;
+ if (!inPrintMode && doPrintMode(contentRootElement)) {
+ LogInfo("MakeProgress: setting up print mode");
+ setupPrintMode();
+ }
+ // Try next state
+ MakeProgress();
+ return;
+
+ case STATE_WAITING_TO_FINISH:
+ LogInfo("MakeProgress: STATE_WAITING_TO_FINISH");
+ if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
+ gFailureReason = "timed out waiting for pending paint count to " +
+ "reach zero (after reftest-wait removed and switch to print mode)";
+ if (shouldWaitForExplicitPaintWaiters()) {
+ gFailureReason += " (waiting for MozPaintWaitFinished)";
+ LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
+ }
+ if (shouldWaitForPendingPaints()) {
+ gFailureReason += " (waiting for MozAfterPaint)";
+ LogInfo("MakeProgress: waiting for MozAfterPaint");
+ }
+ return;
+ }
+ if (contentRootElement) {
+ var elements = getNoPaintElements(contentRootElement);
+ for (var i = 0; i < elements.length; ++i) {
+ if (windowUtils().checkAndClearPaintedState(elements[i])) {
+ SendFailedNoPaint();
+ }
+ }
+ }
+ LogInfo("MakeProgress: Completed");
+ state = STATE_COMPLETED;
+ gFailureReason = "timed out while taking snapshot (bug in harness?)";
+ RemoveListeners();
+ CheckForProcessCrashExpectation();
+ setTimeout(RecordResult, 0);
+ return;
+ }
+ }
+
+ LogInfo("WaitForTestEnd: Adding listeners");
+ addEventListener("MozAfterPaint", AfterPaintListener, false);
+ // If contentRootElement is null then shouldWaitForReftestWaitRemoval will
+ // always return false so we don't need a listener anyway
+ if (contentRootElement) {
+ contentRootElement.addEventListener("DOMAttrModified", AttrModifiedListener, false);
+ }
+ gExplicitPendingPaintsCompleteHook = ExplicitPaintsCompleteListener;
+ gTimeoutHook = RemoveListeners;
+
+ // Listen for spell checks on spell-checked elements.
+ var numPendingSpellChecks = spellCheckedElements.length;
+ function decNumPendingSpellChecks() {
+ --numPendingSpellChecks;
+ MakeProgress();
+ }
+ for (let editable of spellCheckedElements) {
+ try {
+ onSpellCheck(editable, decNumPendingSpellChecks);
+ } catch (err) {
+ // The element may not have an editor, so ignore it.
+ setTimeout(decNumPendingSpellChecks, 0);
+ }
+ }
+
+ // Take a full snapshot now that all our listeners are set up. This
+ // ensures it's impossible for us to miss updates between taking the snapshot
+ // and adding our listeners.
+ SendInitCanvasWithSnapshot();
+ MakeProgress();
+}
+
+function OnDocumentLoad(event)
+{
+ var currentDoc = content.document;
+ if (event.target != currentDoc)
+ // Ignore load events for subframes.
+ return;
+
+ if (gClearingForAssertionCheck &&
+ currentDoc.location.href == BLANK_URL_FOR_CLEARING) {
+ DoAssertionCheck();
+ return;
+ }
+
+ if (currentDoc.location.href != gCurrentURL) {
+ LogInfo("OnDocumentLoad fired for previous document");
+ // Ignore load events for previous documents.
+ return;
+ }
+
+ // Collect all editable, spell-checked elements. It may be the case that
+ // not all the elements that match this selector will be spell checked: for
+ // example, a textarea without a spellcheck attribute may have a parent with
+ // spellcheck=false, or script may set spellcheck=false on an element whose
+ // markup sets it to true. But that's OK since onSpellCheck detects the
+ // absence of spell checking, too.
+ var querySelector =
+ '*[class~="spell-checked"],' +
+ 'textarea:not([spellcheck="false"]),' +
+ 'input[spellcheck]:-moz-any([spellcheck=""],[spellcheck="true"]),' +
+ '*[contenteditable]:-moz-any([contenteditable=""],[contenteditable="true"])';
+ var spellCheckedElements = currentDoc.querySelectorAll(querySelector);
+
+ var contentRootElement = currentDoc ? currentDoc.documentElement : null;
+ currentDoc = null;
+ setupZoom(contentRootElement);
+ setupDisplayport(contentRootElement);
+ var inPrintMode = false;
+
+ function AfterOnLoadScripts() {
+ // Regrab the root element, because the document may have changed.
+ var contentRootElement =
+ content.document ? content.document.documentElement : null;
+
+ // Take a snapshot now. We need to do this before we check whether
+ // we should wait, since this might trigger dispatching of
+ // MozPaintWait events and make shouldWaitForExplicitPaintWaiters() true
+ // below.
+ var painted = SendInitCanvasWithSnapshot();
+
+ if (shouldWaitForExplicitPaintWaiters() ||
+ (!inPrintMode && doPrintMode(contentRootElement)) ||
+ // If we didn't force a paint above, in
+ // InitCurrentCanvasWithSnapshot, so we should wait for a
+ // paint before we consider them done.
+ !painted) {
+ LogInfo("AfterOnLoadScripts belatedly entering WaitForTestEnd");
+ // Go into reftest-wait mode belatedly.
+ WaitForTestEnd(contentRootElement, inPrintMode, []);
+ } else {
+ CheckForProcessCrashExpectation();
+ RecordResult();
+ }
+ }
+
+ if (shouldWaitForReftestWaitRemoval(contentRootElement) ||
+ shouldWaitForExplicitPaintWaiters() ||
+ spellCheckedElements.length) {
+ // Go into reftest-wait mode immediately after painting has been
+ // unsuppressed, after the onload event has finished dispatching.
+ gFailureReason = "timed out waiting for test to complete (trying to get into WaitForTestEnd)";
+ LogInfo("OnDocumentLoad triggering WaitForTestEnd");
+ setTimeout(function () { WaitForTestEnd(contentRootElement, inPrintMode, spellCheckedElements); }, 0);
+ } else {
+ if (doPrintMode(contentRootElement)) {
+ LogInfo("OnDocumentLoad setting up print mode");
+ setupPrintMode();
+ inPrintMode = true;
+ }
+
+ // Since we can't use a bubbling-phase load listener from chrome,
+ // this is a capturing phase listener. So do setTimeout twice, the
+ // first to get us after the onload has fired in the content, and
+ // the second to get us after any setTimeout(foo, 0) in the content.
+ gFailureReason = "timed out waiting for test to complete (waiting for onload scripts to complete)";
+ LogInfo("OnDocumentLoad triggering AfterOnLoadScripts");
+ setTimeout(function () { setTimeout(AfterOnLoadScripts, 0); }, 0);
+ }
+}
+
+function CheckForProcessCrashExpectation()
+{
+ var contentRootElement = content.document.documentElement;
+ if (contentRootElement &&
+ contentRootElement.hasAttribute('class') &&
+ contentRootElement.getAttribute('class').split(/\s+/)
+ .indexOf("reftest-expect-process-crash") != -1) {
+ SendExpectProcessCrash();
+ }
+}
+
+function RecordResult()
+{
+ LogInfo("RecordResult fired");
+
+ var currentTestRunTime = Date.now() - gCurrentTestStartTime;
+
+ clearTimeout(gFailureTimeout);
+ gFailureReason = null;
+ gFailureTimeout = null;
+
+ if (gCurrentTestType == TYPE_SCRIPT) {
+ var error = '';
+ var testwindow = content;
+
+ if (testwindow.wrappedJSObject)
+ testwindow = testwindow.wrappedJSObject;
+
+ var testcases;
+ if (!testwindow.getTestCases || typeof testwindow.getTestCases != "function") {
+ // Force an unexpected failure to alert the test author to fix the test.
+ error = "test must provide a function getTestCases(). (SCRIPT)\n";
+ }
+ else if (!(testcases = testwindow.getTestCases())) {
+ // Force an unexpected failure to alert the test author to fix the test.
+ error = "test's getTestCases() must return an Array-like Object. (SCRIPT)\n";
+ }
+ else if (testcases.length == 0) {
+ // This failure may be due to a JavaScript Engine bug causing
+ // early termination of the test. If we do not allow silent
+ // failure, the driver will report an error.
+ }
+
+ var results = [ ];
+ if (!error) {
+ // FIXME/bug 618176: temporary workaround
+ for (var i = 0; i < testcases.length; ++i) {
+ var test = testcases[i];
+ results.push({ passed: test.testPassed(),
+ description: test.testDescription() });
+ }
+ //results = testcases.map(function(test) {
+ // return { passed: test.testPassed(),
+ // description: test.testDescription() };
+ }
+
+ SendScriptResults(currentTestRunTime, error, results);
+ FinishTestItem();
+ return;
+ }
+
+ SendTestDone(currentTestRunTime);
+ FinishTestItem();
+}
+
+function LoadFailed()
+{
+ if (gTimeoutHook) {
+ gTimeoutHook();
+ }
+ gFailureTimeout = null;
+ SendFailedLoad(gFailureReason);
+}
+
+function FinishTestItem()
+{
+ gHaveCanvasSnapshot = false;
+}
+
+function DoAssertionCheck()
+{
+ gClearingForAssertionCheck = false;
+
+ var numAsserts = 0;
+ if (gDebug.isDebugBuild) {
+ var newAssertionCount = gDebug.assertionCount;
+ numAsserts = newAssertionCount - gAssertionCount;
+ gAssertionCount = newAssertionCount;
+ }
+ SendAssertionCount(numAsserts);
+}
+
+function LoadURI(uri)
+{
+ var flags = webNavigation().LOAD_FLAGS_NONE;
+ webNavigation().loadURI(uri, flags, null, null, null);
+}
+
+function LogWarning(str)
+{
+ if (gVerbose) {
+ sendSyncMessage("reftest:Log", { type: "warning", msg: str });
+ } else {
+ sendAsyncMessage("reftest:Log", { type: "warning", msg: str });
+ }
+}
+
+function LogInfo(str)
+{
+ if (gVerbose) {
+ sendSyncMessage("reftest:Log", { type: "info", msg: str });
+ } else {
+ sendAsyncMessage("reftest:Log", { type: "info", msg: str });
+ }
+}
+
+const SYNC_DEFAULT = 0x0;
+const SYNC_ALLOW_DISABLE = 0x1;
+function SynchronizeForSnapshot(flags)
+{
+ if (gCurrentTestType == TYPE_SCRIPT ||
+ gCurrentTestType == TYPE_LOAD) {
+ // Script tests or load-only tests do not need any snapshotting
+ return;
+ }
+
+ if (flags & SYNC_ALLOW_DISABLE) {
+ var docElt = content.document.documentElement;
+ if (docElt && docElt.hasAttribute("reftest-no-sync-layers")) {
+ LogInfo("Test file chose to skip SynchronizeForSnapshot");
+ return;
+ }
+ }
+
+ var dummyCanvas = content.document.createElementNS(XHTML_NS, "canvas");
+ dummyCanvas.setAttribute("width", 1);
+ dummyCanvas.setAttribute("height", 1);
+
+ var ctx = dummyCanvas.getContext("2d");
+ var flags = ctx.DRAWWINDOW_DRAW_CARET | ctx.DRAWWINDOW_DRAW_VIEW | ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
+ ctx.drawWindow(content, 0, 0, 1, 1, "rgb(255,255,255)", flags);
+}
+
+function RegisterMessageListeners()
+{
+ addMessageListener(
+ "reftest:Clear",
+ function (m) { RecvClear() }
+ );
+ addMessageListener(
+ "reftest:LoadScriptTest",
+ function (m) { RecvLoadScriptTest(m.json.uri, m.json.timeout); }
+ );
+ addMessageListener(
+ "reftest:LoadTest",
+ function (m) { RecvLoadTest(m.json.type, m.json.uri, m.json.timeout); }
+ );
+ addMessageListener(
+ "reftest:ResetRenderingState",
+ function (m) { RecvResetRenderingState(); }
+ );
+}
+
+function RecvClear()
+{
+ gClearingForAssertionCheck = true;
+ LoadURI(BLANK_URL_FOR_CLEARING);
+}
+
+function RecvLoadTest(type, uri, timeout)
+{
+ StartTestURI(type, uri, timeout);
+}
+
+function RecvLoadScriptTest(uri, timeout)
+{
+ StartTestURI(TYPE_SCRIPT, uri, timeout);
+}
+
+function RecvResetRenderingState()
+{
+ resetZoom();
+ resetDisplayport();
+}
+
+function SendAssertionCount(numAssertions)
+{
+ sendAsyncMessage("reftest:AssertionCount", { count: numAssertions });
+}
+
+function SendContentReady()
+{
+ return sendSyncMessage("reftest:ContentReady")[0];
+}
+
+function SendException(what)
+{
+ sendAsyncMessage("reftest:Exception", { what: what });
+}
+
+function SendFailedLoad(why)
+{
+ sendAsyncMessage("reftest:FailedLoad", { why: why });
+}
+
+function SendFailedNoPaint()
+{
+ sendAsyncMessage("reftest:FailedNoPaint");
+}
+
+function SendEnableAsyncScroll()
+{
+ sendAsyncMessage("reftest:EnableAsyncScroll");
+}
+
+// Return true if a snapshot was taken.
+function SendInitCanvasWithSnapshot()
+{
+ // If we're in the same process as the top-level XUL window, then
+ // drawing that window will also update our layers, so no
+ // synchronization is needed.
+ //
+ // NB: this is a test-harness optimization only, it must not
+ // affect the validity of the tests.
+ if (gBrowserIsRemote) {
+ SynchronizeForSnapshot(SYNC_DEFAULT);
+ }
+
+ // For in-process browser, we have to make a synchronous request
+ // here to make the above optimization valid, so that MozWaitPaint
+ // events dispatched (synchronously) during painting are received
+ // before we check the paint-wait counter. For out-of-process
+ // browser though, it doesn't wrt correctness whether this request
+ // is sync or async.
+ var ret = sendSyncMessage("reftest:InitCanvasWithSnapshot")[0];
+
+ gHaveCanvasSnapshot = ret.painted;
+ return ret.painted;
+}
+
+function SendScriptResults(runtimeMs, error, results)
+{
+ sendAsyncMessage("reftest:ScriptResults",
+ { runtimeMs: runtimeMs, error: error, results: results });
+}
+
+function SendExpectProcessCrash(runtimeMs)
+{
+ sendAsyncMessage("reftest:ExpectProcessCrash");
+}
+
+function SendTestDone(runtimeMs)
+{
+ sendAsyncMessage("reftest:TestDone", { runtimeMs: runtimeMs });
+}
+
+function roundTo(x, fraction)
+{
+ return Math.round(x/fraction)*fraction;
+}
+
+function SendUpdateCanvasForEvent(event, contentRootElement)
+{
+ var win = content;
+ var scale = markupDocumentViewer().fullZoom;
+
+ var rects = [ ];
+ if (shouldSnapshotWholePage) {
+ // See comments in SendInitCanvasWithSnapshot() re: the split
+ // logic here.
+ if (!gBrowserIsRemote) {
+ sendSyncMessage("reftest:UpdateWholeCanvasForInvalidation");
+ } else {
+ SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
+ sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
+ }
+ return;
+ }
+
+ var rectList = event.clientRects;
+ LogInfo("SendUpdateCanvasForEvent with " + rectList.length + " rects");
+ for (var i = 0; i < rectList.length; ++i) {
+ var r = rectList[i];
+ // Set left/top/right/bottom to "device pixel" boundaries
+ var left = Math.floor(roundTo(r.left*scale, 0.001));
+ var top = Math.floor(roundTo(r.top*scale, 0.001));
+ var right = Math.ceil(roundTo(r.right*scale, 0.001));
+ var bottom = Math.ceil(roundTo(r.bottom*scale, 0.001));
+ LogInfo("Rect: " + left + " " + top + " " + right + " " + bottom);
+
+ rects.push({ left: left, top: top, right: right, bottom: bottom });
+ }
+
+ // See comments in SendInitCanvasWithSnapshot() re: the split
+ // logic here.
+ if (!gBrowserIsRemote) {
+ sendSyncMessage("reftest:UpdateCanvasForInvalidation", { rects: rects });
+ } else {
+ SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
+ sendAsyncMessage("reftest:UpdateCanvasForInvalidation", { rects: rects });
+ }
+}
+#if REFTEST_B2G
+OnInitialLoad();
+#else
+addEventListener("load", OnInitialLoad, true);
+#endif
diff --git a/layout/tools/reftest/reftest-to-html.pl b/layout/tools/reftest/reftest-to-html.pl
new file mode 100644
index 000000000..3fc2380e9
--- /dev/null
+++ b/layout/tools/reftest/reftest-to-html.pl
@@ -0,0 +1,118 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+print <<EOD
+<html>
+<head>
+<title>reftest output</title>
+<style type="text/css">
+/* must be in this order */
+.PASS { background-color: green; }
+.FAIL { background-color: red; }
+.XFAIL { background-color: #999300; }
+.WEIRDPASS { background-color: #00FFED; }
+.PASSRANDOM { background-color: #598930; }
+.FAILRANDOM, td.XFAILRANDOM { background-color: #99402A; }
+
+.FAILIMAGES { }
+img { margin: 5px; width: 80px; height: 100px; }
+img.testresult { border: 2px solid red; }
+img.testref { border: 2px solid green; }
+a { color: inherit; }
+.always { display: inline ! important; }
+</style>
+</head>
+<body>
+<p>
+<span class="PASS always"><input type="checkbox" checked="true" onclick="var s = document.styleSheets[0].cssRules[0].style; if (s.display == 'none') s.display = null; else s.display = 'none';">PASS</span>&nbsp;
+<span class="FAIL always"><input type="checkbox" checked="true" onclick="var s = document.styleSheets[0].cssRules[1].style; if (s.display == 'none') s.display = null; else s.display = 'none';">UNEXPECTED FAIL</span>&nbsp;
+<span class="XFAIL always"><input type="checkbox" checked="true" onclick="var s = document.styleSheets[0].cssRules[2].style; if (s.display == 'none') s.display = null; else s.display = 'none';">KNOWN FAIL</span>&nbsp;
+<span class="WEIRDPASS always"><input type="checkbox" checked="true" onclick="var s = document.styleSheets[0].cssRules[3].style; if (s.display == 'none') s.display = null; else s.display = 'none';">UNEXPECTED PASS</span>&nbsp;
+<span class="PASSRANDOM always"><input type="checkbox" checked="true" onclick="var s = document.styleSheets[0].cssRules[4].style; if (s.display == 'none') s.display = null; else s.display = 'none';">PASS (Random)</span>&nbsp;
+<span class="FAILRANDOM always"><input type="checkbox" checked="true" onclick="var s = document.styleSheets[0].cssRules[5].style; if (s.display == 'none') s.display = null; else s.display = 'none';">FAIL (Random)</span>&nbsp;
+</p>
+<table>
+EOD
+;
+
+sub readcleanline {
+ my $l = <>;
+ chomp $l;
+ chop $l if ($l =~ /\r$/);
+ return $l;
+}
+
+sub do_html {
+ my ($l) = @_;
+
+ $l =~ s,(file:[^ ]*),<a href="\1">\1</a>,g;
+ $l =~ s,(data:[^ ]*),<a href="\1">\1</a>,g;
+
+ return $l;
+}
+
+$l = 0;
+
+while (<>) {
+ $l++;
+ next unless /^REFTEST/;
+
+ chomp;
+ chop if /\r$/;
+
+ s/^REFTEST *//;
+
+ my $randomresult = 0;
+ if (/EXPECTED RANDOM/) {
+ s/\(EXPECTED RANDOM\)//;
+ $randomresult = 1;
+ }
+
+ if (/^TEST-PASS \| (.*)$/) {
+ my $class = $randomresult ? "PASSRANDOM" : "PASS";
+ print '<tr><td class="' . $class . '">' . do_html($1) . "</td></tr>\n";
+ } elsif (/^TEST-UNEXPECTED-(....) \| (.*)$/) {
+ if ($randomresult) {
+ die "Error on line $l: UNEXPECTED with test marked random?!";
+ }
+ my $class = ($1 eq "PASS") ? "WEIRDPASS" : "FAIL";
+ print '<tr><td class="' . $class . '">' . do_html($2) . "</td></tr>\n";
+
+ # UNEXPECTED results can be followed by one or two images
+ $testline = &readcleanline;
+
+ print '<tr><td class="FAILIMAGES">';
+
+ if ($testline =~ /REFTEST IMAGE: (data:.*)$/) {
+ print '<a href="' . $1 . '"><img class="testresult" src="' . $1 . '"></a>';
+ } elsif ($testline =~ /REFTEST IMAGE 1 \(TEST\): (data:.*)$/) {
+ $refline = &readcleanline;
+ print '<a href="' . $1 . '"><img class="testresult" src="' . $1 . '"></a>';
+ {
+ die "Error on line $l" unless $refline =~ /REFTEST IMAGE 2 \(REFERENCE\): (data:.*)$/;
+ print '<a href="' . $1 . '"><img class="testref" src="' . $1 . '"></a>';
+ }
+
+ } else {
+ die "Error on line $l";
+ }
+
+ print "</td></tr>\n";
+ } elsif (/^TEST-KNOWN-FAIL \| (.*$)/) {
+ my $class = $randomresult ? "XFAILRANDOM" : "XFAIL";
+ print '<tr><td class="' . $class . '">' . do_html($1) . "</td></tr>\n";
+ } else {
+ print STDERR "Unknown Line: " . $_ . "\n";
+ print "<tr><td><pre>" . $_ . "</pre></td></tr>\n";
+ }
+}
+
+print <<EOD
+</table>
+</body>
+</html>
+EOD
+;
diff --git a/layout/tools/reftest/reftest.js b/layout/tools/reftest/reftest.js
new file mode 100644
index 000000000..7191df1cb
--- /dev/null
+++ b/layout/tools/reftest/reftest.js
@@ -0,0 +1,1957 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- /
+/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if BOOTSTRAP
+this.EXPORTED_SYMBOLS = ["OnRefTestLoad"];
+#endif
+
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CR = Components.results;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const NS_LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+const NS_GFXINFO_CONTRACTID = "@mozilla.org/gfx/info;1";
+const IO_SERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
+const DEBUG_CONTRACTID = "@mozilla.org/xpcom/debug;1";
+const NS_LOCALFILEINPUTSTREAM_CONTRACTID =
+ "@mozilla.org/network/file-input-stream;1";
+const NS_SCRIPTSECURITYMANAGER_CONTRACTID =
+ "@mozilla.org/scriptsecuritymanager;1";
+const NS_REFTESTHELPER_CONTRACTID =
+ "@mozilla.org/reftest-helper;1";
+const NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX =
+ "@mozilla.org/network/protocol;1?name=";
+const NS_XREAPPINFO_CONTRACTID =
+ "@mozilla.org/xre/app-info;1";
+const NS_DIRECTORY_SERVICE_CONTRACTID =
+ "@mozilla.org/file/directory_service;1";
+const NS_OBSERVER_SERVICE_CONTRACTID =
+ "@mozilla.org/observer-service;1";
+
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+var gLoadTimeout = 0;
+var gTimeoutHook = null;
+var gRemote = false;
+var gIgnoreWindowSize = false;
+var gTotalChunks = 0;
+var gThisChunk = 0;
+var gContainingWindow = null;
+var gURLFilterRegex = null;
+const FOCUS_FILTER_ALL_TESTS = "all";
+const FOCUS_FILTER_NEEDS_FOCUS_TESTS = "needs-focus";
+const FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS = "non-needs-focus";
+var gFocusFilterMode = FOCUS_FILTER_ALL_TESTS;
+
+// "<!--CLEAR-->"
+const BLANK_URL_FOR_CLEARING = "data:text/html;charset=UTF-8,%3C%21%2D%2DCLEAR%2D%2D%3E";
+
+var gBrowser;
+// Are we testing web content loaded in a separate process?
+var gBrowserIsRemote; // bool
+// Are we using <iframe mozbrowser>?
+var gBrowserIsIframe; // bool
+var gBrowserMessageManager;
+var gCanvas1, gCanvas2;
+// gCurrentCanvas is non-null between InitCurrentCanvasWithSnapshot and the next
+// RecordResult.
+var gCurrentCanvas = null;
+var gURLs;
+// Map from URI spec to the number of times it remains to be used
+var gURIUseCounts;
+// Map from URI spec to the canvas rendered for that URI
+var gURICanvases;
+var gTestResults = {
+ // Successful...
+ Pass: 0,
+ LoadOnly: 0,
+ // Unexpected...
+ Exception: 0,
+ FailedLoad: 0,
+ UnexpectedFail: 0,
+ UnexpectedPass: 0,
+ AssertionUnexpected: 0,
+ AssertionUnexpectedFixed: 0,
+ // Known problems...
+ KnownFail : 0,
+ AssertionKnown: 0,
+ Random : 0,
+ Skip: 0,
+ Slow: 0,
+};
+var gTotalTests = 0;
+var gState;
+var gCurrentURL;
+var gTestLog = [];
+var gServer;
+var gCount = 0;
+var gAssertionCount = 0;
+
+var gIOService;
+var gDebug;
+var gWindowUtils;
+
+var gSlowestTestTime = 0;
+var gSlowestTestURL;
+
+var gDrawWindowFlags;
+
+var gExpectingProcessCrash = false;
+var gExpectedCrashDumpFiles = [];
+var gUnexpectedCrashDumpFiles = { };
+var gCrashDumpDir;
+var gFailedNoPaint = false;
+
+const TYPE_REFTEST_EQUAL = '==';
+const TYPE_REFTEST_NOTEQUAL = '!=';
+const TYPE_LOAD = 'load'; // test without a reference (just test that it does
+ // not assert, crash, hang, or leak)
+const TYPE_SCRIPT = 'script'; // test contains individual test results
+
+// The order of these constants matters, since when we have a status
+// listed for a *manifest*, we combine the status with the status for
+// the test by using the *larger*.
+// FIXME: In the future, we may also want to use this rule for combining
+// statuses that are on the same line (rather than making the last one
+// win).
+const EXPECTED_PASS = 0;
+const EXPECTED_FAIL = 1;
+const EXPECTED_RANDOM = 2;
+const EXPECTED_DEATH = 3; // test must be skipped to avoid e.g. crash/hang
+const EXPECTED_FUZZY = 4;
+
+// types of preference value we might want to set for a specific test
+const PREF_BOOLEAN = 0;
+const PREF_STRING = 1;
+const PREF_INTEGER = 2;
+
+var gPrefsToRestore = [];
+
+const gProtocolRE = /^\w+:/;
+const gPrefItemRE = /^(|test-|ref-)pref\((.+?),(.*)\)$/;
+
+var gHttpServerPort = -1;
+
+// whether to run slow tests or not
+var gRunSlowTests = true;
+
+// whether we should skip caching canvases
+var gNoCanvasCache = false;
+
+var gRecycledCanvases = new Array();
+
+// By default we just log to stdout
+var gDumpLog = dump;
+var gVerbose = false;
+
+// Only dump the sandbox once, because it doesn't depend on the
+// manifest URL (yet!).
+var gDumpedConditionSandbox = false;
+
+function LogWarning(str)
+{
+ gDumpLog("REFTEST INFO | " + str + "\n");
+ gTestLog.push(str);
+}
+
+function LogInfo(str)
+{
+ if (gVerbose)
+ gDumpLog("REFTEST INFO | " + str + "\n");
+ gTestLog.push(str);
+}
+
+function FlushTestLog()
+{
+ if (!gVerbose) {
+ // In verbose mode, we've dumped all these messages already.
+ for (var i = 0; i < gTestLog.length; ++i) {
+ gDumpLog("REFTEST INFO | Saved log: " + gTestLog[i] + "\n");
+ }
+ }
+ gTestLog = [];
+}
+
+function AllocateCanvas()
+{
+ if (gRecycledCanvases.length > 0)
+ return gRecycledCanvases.shift();
+
+ var canvas = gContainingWindow.document.createElementNS(XHTML_NS, "canvas");
+ var r = gBrowser.getBoundingClientRect();
+ canvas.setAttribute("width", Math.ceil(r.width));
+ canvas.setAttribute("height", Math.ceil(r.height));
+
+ return canvas;
+}
+
+function ReleaseCanvas(canvas)
+{
+ // store a maximum of 2 canvases, if we're not caching
+ if (!gNoCanvasCache || gRecycledCanvases.length < 2)
+ gRecycledCanvases.push(canvas);
+}
+
+function IDForEventTarget(event)
+{
+ try {
+ return "'" + event.target.getAttribute('id') + "'";
+ } catch (ex) {
+ return "<unknown>";
+ }
+}
+
+this.OnRefTestLoad = function OnRefTestLoad(win)
+{
+ gCrashDumpDir = CC[NS_DIRECTORY_SERVICE_CONTRACTID]
+ .getService(CI.nsIProperties)
+ .get("ProfD", CI.nsIFile);
+ gCrashDumpDir.append("minidumps");
+
+ var env = CC["@mozilla.org/process/environment;1"].
+ getService(CI.nsIEnvironment);
+ gVerbose = !!env.get("MOZ_REFTEST_VERBOSE");
+
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ try {
+ gBrowserIsRemote = prefs.getBoolPref("browser.tabs.remote");
+ } catch (e) {
+ gBrowserIsRemote = false;
+ }
+
+ try {
+ gBrowserIsIframe = prefs.getBoolPref("reftest.browser.iframe.enabled");
+ } catch (e) {
+ gBrowserIsIframe = false;
+ }
+
+ if (win === undefined || win == null) {
+ win = window;
+ }
+ if (gContainingWindow == null && win != null) {
+ gContainingWindow = win;
+ }
+
+ if (gBrowserIsIframe) {
+ gBrowser = gContainingWindow.document.createElementNS(XHTML_NS, "iframe");
+ gBrowser.setAttribute("mozbrowser", "");
+ } else {
+ gBrowser = gContainingWindow.document.createElementNS(XUL_NS, "xul:browser");
+ }
+ gBrowser.setAttribute("id", "browser");
+ gBrowser.setAttribute("type", "content-primary");
+ gBrowser.setAttribute("remote", gBrowserIsRemote ? "true" : "false");
+ // Make sure the browser element is exactly 800x1000, no matter
+ // what size our window is
+ gBrowser.setAttribute("style", "min-width: 800px; min-height: 1000px; max-width: 800px; max-height: 1000px");
+
+#if BOOTSTRAP
+#if REFTEST_B2G
+ var doc = gContainingWindow.document.getElementsByTagName("window")[0];
+#else
+ var doc = gContainingWindow.document.getElementById('main-window');
+#endif
+ while (doc.hasChildNodes()) {
+ doc.removeChild(doc.firstChild);
+ }
+ doc.appendChild(gBrowser);
+#else
+ document.getElementById("reftest-window").appendChild(gBrowser);
+#endif
+
+ gBrowserMessageManager = gBrowser.QueryInterface(CI.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ // The content script waits for the initial onload, then notifies
+ // us.
+ RegisterMessageListenersAndLoadContentScript();
+}
+
+function InitAndStartRefTests()
+{
+ /* These prefs are optional, so we don't need to spit an error to the log */
+ try {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ } catch(e) {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + e + "\n");
+ }
+
+ try {
+ prefs.setBoolPref("android.widget_paints_background", false);
+ } catch (e) {}
+
+ /* set the gLoadTimeout */
+ try {
+ gLoadTimeout = prefs.getIntPref("reftest.timeout");
+ } catch(e) {
+ gLoadTimeout = 5 * 60 * 1000; //5 minutes as per bug 479518
+ }
+
+ /* Get the logfile for android tests */
+ try {
+ var logFile = prefs.getCharPref("reftest.logFile");
+ if (logFile) {
+ try {
+ var f = FileUtils.File(logFile);
+ var mfl = FileUtils.openFileOutputStream(f, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
+ // Set to mirror to stdout as well as the file
+ gDumpLog = function (msg) {
+#if BOOTSTRAP
+#if REFTEST_B2G
+ dump(msg);
+#else
+ //NOTE: on android-xul, we have a libc crash if we do a dump with %7s in the string
+#endif
+#else
+ dump(msg);
+#endif
+ mfl.write(msg, msg.length);
+ };
+ }
+ catch(e) {
+ // If there is a problem, just use stdout
+ gDumpLog = dump;
+ }
+ }
+ } catch(e) {}
+
+ try {
+ gRemote = prefs.getBoolPref("reftest.remote");
+ } catch(e) {
+ gRemote = false;
+ }
+
+ try {
+ gIgnoreWindowSize = prefs.getBoolPref("reftest.ignoreWindowSize");
+ } catch(e) {
+ gIgnoreWindowSize = false;
+ }
+
+ /* Support for running a chunk (subset) of tests. In separate try as this is optional */
+ try {
+ gTotalChunks = prefs.getIntPref("reftest.totalChunks");
+ gThisChunk = prefs.getIntPref("reftest.thisChunk");
+ }
+ catch(e) {
+ gTotalChunks = 0;
+ gThisChunk = 0;
+ }
+
+ try {
+ gURLFilterRegex = new RegExp(prefs.getCharPref("reftest.filter"));
+ } catch(e) {}
+
+ try {
+ gFocusFilterMode = prefs.getCharPref("reftest.focusFilterMode");
+ } catch(e) {}
+
+ gWindowUtils = gContainingWindow.QueryInterface(CI.nsIInterfaceRequestor).getInterface(CI.nsIDOMWindowUtils);
+ if (!gWindowUtils || !gWindowUtils.compareCanvases)
+ throw "nsIDOMWindowUtils inteface missing";
+
+ gIOService = CC[IO_SERVICE_CONTRACTID].getService(CI.nsIIOService);
+ gDebug = CC[DEBUG_CONTRACTID].getService(CI.nsIDebug2);
+
+ RegisterProcessCrashObservers();
+
+ if (gRemote) {
+ gServer = null;
+ } else {
+ gServer = CC["@mozilla.org/server/jshttp;1"].
+ createInstance(CI.nsIHttpServer);
+ }
+ try {
+ if (gServer)
+ StartHTTPServer();
+ } catch (ex) {
+ //gBrowser.loadURI('data:text/plain,' + ex);
+ ++gTestResults.Exception;
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + ex + "\n");
+ DoneTests();
+ }
+
+ // Focus the content browser
+ gBrowser.focus();
+
+ StartTests();
+}
+
+function StartHTTPServer()
+{
+ gServer.registerContentType("sjs", "sjs");
+ gServer.start(-1);
+ gHttpServerPort = gServer.identity.primaryPort;
+}
+
+function StartTests()
+{
+ var uri;
+#if BOOTSTRAP
+ /* These prefs are optional, so we don't need to spit an error to the log */
+ try {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ } catch(e) {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + e + "\n");
+ }
+
+ try {
+ gNoCanvasCache = prefs.getIntPref("reftest.nocache");
+ } catch(e) {
+ gNoCanvasCache = false;
+ }
+
+ try {
+ gRunSlowTests = prefs.getIntPref("reftest.skipslowtests");
+ } catch(e) {
+ gRunSlowTests = false;
+ }
+
+ try {
+ uri = prefs.getCharPref("reftest.uri");
+ } catch(e) {
+ uri = "";
+ }
+
+ if (uri == "") {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | Unable to find reftest.uri pref. Please ensure your profile is setup properly\n");
+ DoneTests();
+ }
+#else
+ try {
+ // Need to read the manifest once we have gHttpServerPort..
+ var args = window.arguments[0].wrappedJSObject;
+
+ if ("nocache" in args && args["nocache"])
+ gNoCanvasCache = true;
+
+ if ("skipslowtests" in args && args.skipslowtests)
+ gRunSlowTests = false;
+
+ uri = args.uri;
+ } catch (e) {
+ ++gTestResults.Exception;
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + ex + "\n");
+ DoneTests();
+ }
+#endif
+ try {
+ ReadTopManifest(uri);
+ BuildUseCounts();
+
+ // Filter tests which will be skipped to get a more even distribution when chunking
+ // tURLs is a temporary array containing all active tests
+ var tURLs = new Array();
+ for (var i = 0; i < gURLs.length; ++i) {
+ if (gURLs[i].expected == EXPECTED_DEATH)
+ continue;
+
+ if (gURLs[i].needsFocus && !Focus())
+ continue;
+
+ if (gURLs[i].slow && !gRunSlowTests)
+ continue;
+
+ tURLs.push(gURLs[i]);
+ }
+
+ gDumpLog("REFTEST INFO | Discovered " + gURLs.length + " tests, after filtering SKIP tests, we have " + tURLs.length + "\n");
+
+ if (gTotalChunks > 0 && gThisChunk > 0) {
+ // Calculate start and end indices of this chunk if tURLs array were
+ // divided evenly
+ var testsPerChunk = tURLs.length / gTotalChunks;
+ var start = Math.round((gThisChunk-1) * testsPerChunk);
+ var end = Math.round(gThisChunk * testsPerChunk);
+
+ // Map these indices onto the gURLs array. This avoids modifying the
+ // gURLs array which prevents skipped tests from showing up in the log
+ start = gThisChunk == 1 ? 0 : gURLs.indexOf(tURLs[start]);
+ end = gThisChunk == gTotalChunks ? gURLs.length : gURLs.indexOf(tURLs[end + 1]) - 1;
+ gURLs = gURLs.slice(start, end);
+
+ gDumpLog("REFTEST INFO | Running chunk " + gThisChunk + " out of " + gTotalChunks + " chunks. ");
+ gDumpLog("tests " + (start+1) + "-" + end + "/" + gURLs.length + "\n");
+ }
+ gTotalTests = gURLs.length;
+
+ if (!gTotalTests)
+ throw "No tests to run";
+
+ gURICanvases = {};
+ StartCurrentTest();
+ } catch (ex) {
+ //gBrowser.loadURI('data:text/plain,' + ex);
+ ++gTestResults.Exception;
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + ex + "\n");
+ DoneTests();
+ }
+}
+
+function OnRefTestUnload()
+{
+}
+
+// Read all available data from an input stream and return it
+// as a string.
+function getStreamContent(inputStream)
+{
+ var streamBuf = "";
+ var sis = CC["@mozilla.org/scriptableinputstream;1"].
+ createInstance(CI.nsIScriptableInputStream);
+ sis.init(inputStream);
+
+ var available;
+ while ((available = sis.available()) != 0) {
+ streamBuf += sis.read(available);
+ }
+
+ return streamBuf;
+}
+
+// Build the sandbox for fails-if(), etc., condition evaluation.
+function BuildConditionSandbox(aURL) {
+ var sandbox = new Components.utils.Sandbox(aURL.spec);
+ var xr = CC[NS_XREAPPINFO_CONTRACTID].getService(CI.nsIXULRuntime);
+ sandbox.isDebugBuild = gDebug.isDebugBuild;
+ sandbox.xulRuntime = {widgetToolkit: xr.widgetToolkit, OS: xr.OS, __exposedProps__: { widgetToolkit: "r", OS: "r", XPCOMABI: "r", shell: "r" } };
+
+ // xr.XPCOMABI throws exception for configurations without full ABI
+ // support (mobile builds on ARM)
+ try {
+ sandbox.xulRuntime.XPCOMABI = xr.XPCOMABI;
+ } catch(e) {
+ sandbox.xulRuntime.XPCOMABI = "";
+ }
+
+ var testRect = gBrowser.getBoundingClientRect();
+ sandbox.smallScreen = false;
+ if (gContainingWindow.innerWidth < 800 || gContainingWindow.innerHeight < 1000) {
+ sandbox.smallScreen = true;
+ }
+
+#if REFTEST_B2G
+ // XXX nsIGfxInfo isn't available in B2G
+ sandbox.d2d = false;
+ sandbox.azureQuartz = false;
+ sandbox.azureSkia = false;
+ sandbox.contentSameGfxBackendAsCanvas = false;
+#else
+ var gfxInfo = (NS_GFXINFO_CONTRACTID in CC) && CC[NS_GFXINFO_CONTRACTID].getService(CI.nsIGfxInfo);
+ try {
+ sandbox.d2d = gfxInfo.D2DEnabled;
+ } catch (e) {
+ sandbox.d2d = false;
+ }
+ var info = gfxInfo.getInfo();
+ sandbox.azureQuartz = info.AzureCanvasBackend == "quartz";
+ sandbox.azureSkia = info.AzureCanvasBackend == "skia";
+ // true if we are using the same Azure backend for rendering canvas and content
+ sandbox.contentSameGfxBackendAsCanvas = info.AzureContentBackend == info.AzureCanvasBackend
+ || (info.AzureContentBackend == "none" && info.AzureCanvasBackend == "cairo");
+#endif
+
+ sandbox.layersGPUAccelerated =
+ gWindowUtils.layerManagerType != "Basic";
+ sandbox.layersOpenGL =
+ gWindowUtils.layerManagerType == "OpenGL";
+ sandbox.layersOMTC =
+ gWindowUtils.layerManagerRemote == true;
+
+ // Shortcuts for widget toolkits.
+ sandbox.B2G = xr.widgetToolkit == "gonk";
+ sandbox.Android = xr.OS == "Android" && !sandbox.B2G;
+ sandbox.cocoaWidget = xr.widgetToolkit == "cocoa";
+ sandbox.gtk2Widget = xr.widgetToolkit == "gtk2";
+ sandbox.qtWidget = xr.widgetToolkit == "qt";
+ sandbox.winWidget = xr.widgetToolkit == "windows";
+
+#if MOZ_ASAN
+ sandbox.AddressSanitizer = true;
+#else
+ sandbox.AddressSanitizer = false;
+#endif
+
+ var hh = CC[NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX + "http"].
+ getService(CI.nsIHttpProtocolHandler);
+ sandbox.http = { __exposedProps__: {} };
+ for each (var prop in [ "userAgent", "appName", "appVersion",
+ "vendor", "vendorSub",
+ "product", "productSub",
+ "platform", "oscpu", "language", "misc" ]) {
+ sandbox.http[prop] = hh[prop];
+ sandbox.http.__exposedProps__[prop] = "r";
+ }
+
+ // Set OSX to the Mac OS X version for Mac, and 0 otherwise.
+ var osxmatch = /Mac OS X (\d+.\d+)$/.exec(hh.oscpu);
+ sandbox.OSX = osxmatch ? parseFloat(osxmatch[1]) : 0;
+
+ // see if we have the test plugin available,
+ // and set a sandox prop accordingly
+ sandbox.haveTestPlugin = false;
+
+ var navigator = gContainingWindow.navigator;
+ for (var i = 0; i < navigator.mimeTypes.length; i++) {
+ if (navigator.mimeTypes[i].type == "application/x-test" &&
+ navigator.mimeTypes[i].enabledPlugin != null &&
+ navigator.mimeTypes[i].enabledPlugin.name == "Test Plug-in") {
+ sandbox.haveTestPlugin = true;
+ break;
+ }
+ }
+
+ // Set a flag on sandbox if the windows default theme is active
+ var box = gContainingWindow.document.createElement("box");
+ box.setAttribute("id", "_box_windowsDefaultTheme");
+ gContainingWindow.document.documentElement.appendChild(box);
+ sandbox.windowsDefaultTheme = (gContainingWindow.getComputedStyle(box, null).display == "none");
+ gContainingWindow.document.documentElement.removeChild(box);
+
+ var prefs = CC["@mozilla.org/preferences-service;1"].
+ getService(CI.nsIPrefBranch);
+ try {
+ sandbox.nativeThemePref = !prefs.getBoolPref("mozilla.widget.disable-native-theme");
+ } catch (e) {
+ sandbox.nativeThemePref = true;
+ }
+
+ sandbox.prefs = {
+ __exposedProps__: {
+ getBoolPref: 'r',
+ getIntPref: 'r',
+ },
+ _prefs: prefs,
+ getBoolPref: function(p) { return this._prefs.getBoolPref(p); },
+ getIntPref: function(p) { return this._prefs.getIntPref(p); }
+ }
+
+ sandbox.testPluginIsOOP = function () {
+ try {
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ } catch (ex) {}
+
+ var prefservice = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(CI.nsIPrefBranch);
+
+ var testPluginIsOOP = false;
+ if (navigator.platform.indexOf("Mac") == 0) {
+ var xulRuntime = Components.classes["@mozilla.org/xre/app-info;1"]
+ .getService(CI.nsIXULAppInfo)
+ .QueryInterface(CI.nsIXULRuntime);
+ if (xulRuntime.XPCOMABI.match(/x86-/)) {
+ try {
+ testPluginIsOOP = prefservice.getBoolPref("dom.ipc.plugins.enabled.i386.test.plugin");
+ } catch (e) {
+ testPluginIsOOP = prefservice.getBoolPref("dom.ipc.plugins.enabled.i386");
+ }
+ }
+ else if (xulRuntime.XPCOMABI.match(/x86_64-/)) {
+ try {
+ testPluginIsOOP = prefservice.getBoolPref("dom.ipc.plugins.enabled.x86_64.test.plugin");
+ } catch (e) {
+ testPluginIsOOP = prefservice.getBoolPref("dom.ipc.plugins.enabled.x86_64");
+ }
+ }
+ }
+ else {
+ testPluginIsOOP = prefservice.getBoolPref("dom.ipc.plugins.enabled");
+ }
+
+ return testPluginIsOOP;
+ };
+
+ // Tests shouldn't care about this except for when they need to
+ // crash the content process
+ sandbox.browserIsRemote = gBrowserIsRemote;
+
+ // Distinguish the Fennecs:
+ sandbox.xulFennec = sandbox.Android && sandbox.browserIsRemote;
+ sandbox.nativeFennec = sandbox.Android && !sandbox.browserIsRemote;
+
+ if (!gDumpedConditionSandbox) {
+ dump("REFTEST INFO | Dumping JSON representation of sandbox \n");
+ dump("REFTEST INFO | " + JSON.stringify(sandbox) + " \n");
+ gDumpedConditionSandbox = true;
+ }
+ return sandbox;
+}
+
+function AddPrefSettings(aWhere, aPrefName, aPrefValExpression, aSandbox, aTestPrefSettings, aRefPrefSettings)
+{
+ var prefVal = Components.utils.evalInSandbox("(" + aPrefValExpression + ")", aSandbox);
+ var prefType;
+ var valType = typeof(prefVal);
+ if (valType == "boolean") {
+ prefType = PREF_BOOLEAN;
+ } else if (valType == "string") {
+ prefType = PREF_STRING;
+ } else if (valType == "number" && (parseInt(prefVal) == prefVal)) {
+ prefType = PREF_INTEGER;
+ } else {
+ return false;
+ }
+ var setting = { name: aPrefName,
+ type: prefType,
+ value: prefVal };
+ if (aWhere != "ref-") {
+ aTestPrefSettings.push(setting);
+ }
+ if (aWhere != "test-") {
+ aRefPrefSettings.push(setting);
+ }
+ return true;
+}
+
+function ReadTopManifest(aFileURL)
+{
+ gURLs = new Array();
+ var url = gIOService.newURI(aFileURL, null, null);
+ if (!url)
+ throw "Expected a file or http URL for the manifest.";
+ ReadManifest(url, EXPECTED_PASS);
+}
+
+function AddTestItem(aTest)
+{
+ if (gURLFilterRegex && !gURLFilterRegex.test(aTest.url1.spec))
+ return;
+ if (gFocusFilterMode == FOCUS_FILTER_NEEDS_FOCUS_TESTS &&
+ !aTest.needsFocus)
+ return;
+ if (gFocusFilterMode == FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS &&
+ aTest.needsFocus)
+ return;
+ gURLs.push(aTest);
+}
+
+// Note: If you materially change the reftest manifest parsing,
+// please keep the parser in print-manifest-dirs.py in sync.
+function ReadManifest(aURL, inherited_status)
+{
+ var secMan = CC[NS_SCRIPTSECURITYMANAGER_CONTRACTID]
+ .getService(CI.nsIScriptSecurityManager);
+
+ var listURL = aURL;
+ var channel = gIOService.newChannelFromURI(aURL);
+ var inputStream = channel.open();
+ if (channel instanceof Components.interfaces.nsIHttpChannel
+ && channel.responseStatus != 200) {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | | HTTP ERROR : " +
+ channel.responseStatus + "\n");
+ }
+ var streamBuf = getStreamContent(inputStream);
+ inputStream.close();
+ var lines = streamBuf.split(/\n|\r|\r\n/);
+
+ // Build the sandbox for fails-if(), etc., condition evaluation.
+ var sandbox = BuildConditionSandbox(aURL);
+ var lineNo = 0;
+ var urlprefix = "";
+ var defaultTestPrefSettings = [], defaultRefPrefSettings = [];
+ for each (var str in lines) {
+ ++lineNo;
+ if (str.charAt(0) == "#")
+ continue; // entire line was a comment
+ var i = str.search(/\s+#/);
+ if (i >= 0)
+ str = str.substring(0, i);
+ // strip leading and trailing whitespace
+ str = str.replace(/^\s*/, '').replace(/\s*$/, '');
+ if (!str || str == "")
+ continue;
+ var items = str.split(/\s+/); // split on whitespace
+
+ if (items[0] == "url-prefix") {
+ if (items.length != 2)
+ throw "url-prefix requires one url in manifest file " + aURL.spec + " line " + lineNo;
+ urlprefix = items[1];
+ continue;
+ }
+
+ if (items[0] == "default-preferences") {
+ var m;
+ var item;
+ defaultTestPrefSettings = [];
+ defaultRefPrefSettings = [];
+ items.shift();
+ while ((item = items.shift())) {
+ if (!(m = item.match(gPrefItemRE))) {
+ throw "Unexpected item in default-preferences list in manifest file " + aURL.spec + " line " + lineNo;
+ }
+ if (!AddPrefSettings(m[1], m[2], m[3], sandbox, defaultTestPrefSettings, defaultRefPrefSettings)) {
+ throw "Error in pref value in manifest file " + aURL.spec + " line " + lineNo;
+ }
+ }
+ continue;
+ }
+
+ var expected_status = EXPECTED_PASS;
+ var allow_silent_fail = false;
+ var minAsserts = 0;
+ var maxAsserts = 0;
+ var needs_focus = false;
+ var slow = false;
+ var testPrefSettings = defaultTestPrefSettings.concat();
+ var refPrefSettings = defaultRefPrefSettings.concat();
+ var fuzzy_max_delta = 2;
+ var fuzzy_max_pixels = 1;
+
+ while (items[0].match(/^(fails|needs-focus|random|skip|asserts|slow|require-or|silentfail|pref|test-pref|ref-pref|fuzzy)/)) {
+ var item = items.shift();
+ var stat;
+ var cond;
+ var m = item.match(/^(fails|random|skip|silentfail)-if(\(.*\))$/);
+ if (m) {
+ stat = m[1];
+ // Note: m[2] contains the parentheses, and we want them.
+ cond = Components.utils.evalInSandbox(m[2], sandbox);
+ } else if (item.match(/^(fails|random|skip)$/)) {
+ stat = item;
+ cond = true;
+ } else if (item == "needs-focus") {
+ needs_focus = true;
+ cond = false;
+ } else if ((m = item.match(/^asserts\((\d+)(-\d+)?\)$/))) {
+ cond = false;
+ minAsserts = Number(m[1]);
+ maxAsserts = (m[2] == undefined) ? minAsserts
+ : Number(m[2].substring(1));
+ } else if ((m = item.match(/^asserts-if\((.*?),(\d+)(-\d+)?\)$/))) {
+ cond = false;
+ if (Components.utils.evalInSandbox("(" + m[1] + ")", sandbox)) {
+ minAsserts = Number(m[2]);
+ maxAsserts =
+ (m[3] == undefined) ? minAsserts
+ : Number(m[3].substring(1));
+ }
+ } else if (item == "slow") {
+ cond = false;
+ slow = true;
+ } else if ((m = item.match(/^require-or\((.*?)\)$/))) {
+ var args = m[1].split(/,/);
+ if (args.length != 2) {
+ throw "Error 7 in manifest file " + aURL.spec + " line " + lineNo + ": wrong number of args to require-or";
+ }
+ var [precondition_str, fallback_action] = args;
+ var preconditions = precondition_str.split(/&&/);
+ cond = false;
+ for each (var precondition in preconditions) {
+ if (precondition === "debugMode") {
+ // Currently unimplemented. Requires asynchronous
+ // JSD call + getting an event while no JS is running
+ stat = fallback_action;
+ cond = true;
+ break;
+ } else if (precondition === "true") {
+ // For testing
+ } else {
+ // Unknown precondition. Assume it is unimplemented.
+ stat = fallback_action;
+ cond = true;
+ break;
+ }
+ }
+ } else if ((m = item.match(/^slow-if\((.*?)\)$/))) {
+ cond = false;
+ if (Components.utils.evalInSandbox("(" + m[1] + ")", sandbox))
+ slow = true;
+ } else if (item == "silentfail") {
+ cond = false;
+ allow_silent_fail = true;
+ } else if ((m = item.match(gPrefItemRE))) {
+ cond = false;
+ if (!AddPrefSettings(m[1], m[2], m[3], sandbox, testPrefSettings, refPrefSettings)) {
+ throw "Error in pref value in manifest file " + aURL.spec + " line " + lineNo;
+ }
+ } else if ((m = item.match(/^fuzzy\((\d+),(\d+)\)$/))) {
+ cond = false;
+ expected_status = EXPECTED_FUZZY;
+ fuzzy_max_delta = Number(m[1]);
+ fuzzy_max_pixels = Number(m[2]);
+ } else if ((m = item.match(/^fuzzy-if\((.*?),(\d+),(\d+)\)$/))) {
+ cond = false;
+ if (Components.utils.evalInSandbox("(" + m[1] + ")", sandbox)) {
+ expected_status = EXPECTED_FUZZY;
+ fuzzy_max_delta = Number(m[2]);
+ fuzzy_max_pixels = Number(m[3]);
+ }
+ } else {
+ throw "Error 1 in manifest file " + aURL.spec + " line " + lineNo;
+ }
+
+ if (cond) {
+ if (stat == "fails") {
+ expected_status = EXPECTED_FAIL;
+ } else if (stat == "random") {
+ expected_status = EXPECTED_RANDOM;
+ } else if (stat == "skip") {
+ expected_status = EXPECTED_DEATH;
+ } else if (stat == "silentfail") {
+ allow_silent_fail = true;
+ }
+ }
+ }
+
+ expected_status = Math.max(expected_status, inherited_status);
+
+ if (minAsserts > maxAsserts) {
+ throw "Bad range in manifest file " + aURL.spec + " line " + lineNo;
+ }
+
+ var runHttp = false;
+ var httpDepth;
+ if (items[0] == "HTTP") {
+ runHttp = (aURL.scheme == "file"); // We can't yet run the local HTTP server
+ // for non-local reftests.
+ httpDepth = 0;
+ items.shift();
+ } else if (items[0].match(/HTTP\(\.\.(\/\.\.)*\)/)) {
+ // Accept HTTP(..), HTTP(../..), HTTP(../../..), etc.
+ runHttp = (aURL.scheme == "file"); // We can't yet run the local HTTP server
+ // for non-local reftests.
+ httpDepth = (items[0].length - 5) / 3;
+ items.shift();
+ }
+
+ // do not prefix the url for include commands or urls specifying
+ // a protocol
+ if (urlprefix && items[0] != "include") {
+ if (items.length > 1 && !items[1].match(gProtocolRE)) {
+ items[1] = urlprefix + items[1];
+ }
+ if (items.length > 2 && !items[2].match(gProtocolRE)) {
+ items[2] = urlprefix + items[2];
+ }
+ }
+
+ var principal = secMan.getSimpleCodebasePrincipal(aURL);
+
+ if (items[0] == "include") {
+ if (items.length != 2 || runHttp)
+ throw "Error 2 in manifest file " + aURL.spec + " line " + lineNo;
+ var incURI = gIOService.newURI(items[1], null, listURL);
+ secMan.checkLoadURIWithPrincipal(principal, incURI,
+ CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ ReadManifest(incURI, expected_status);
+ } else if (items[0] == TYPE_LOAD) {
+ if (items.length != 2 ||
+ (expected_status != EXPECTED_PASS &&
+ expected_status != EXPECTED_DEATH))
+ throw "Error 3 in manifest file " + aURL.spec + " line " + lineNo;
+ var [testURI] = runHttp
+ ? ServeFiles(principal, httpDepth,
+ listURL, [items[1]])
+ : [gIOService.newURI(items[1], null, listURL)];
+ var prettyPath = runHttp
+ ? gIOService.newURI(items[1], null, listURL).spec
+ : testURI.spec;
+ secMan.checkLoadURIWithPrincipal(principal, testURI,
+ CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ AddTestItem({ type: TYPE_LOAD,
+ expected: expected_status,
+ allowSilentFail: allow_silent_fail,
+ prettyPath: prettyPath,
+ minAsserts: minAsserts,
+ maxAsserts: maxAsserts,
+ needsFocus: needs_focus,
+ slow: slow,
+ prefSettings1: testPrefSettings,
+ prefSettings2: refPrefSettings,
+ fuzzyMaxDelta: fuzzy_max_delta,
+ fuzzyMaxPixels: fuzzy_max_pixels,
+ url1: testURI,
+ url2: null });
+ } else if (items[0] == TYPE_SCRIPT) {
+ if (items.length != 2)
+ throw "Error 4 in manifest file " + aURL.spec + " line " + lineNo;
+ var [testURI] = runHttp
+ ? ServeFiles(principal, httpDepth,
+ listURL, [items[1]])
+ : [gIOService.newURI(items[1], null, listURL)];
+ var prettyPath = runHttp
+ ? gIOService.newURI(items[1], null, listURL).spec
+ : testURI.spec;
+ secMan.checkLoadURIWithPrincipal(principal, testURI,
+ CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ AddTestItem({ type: TYPE_SCRIPT,
+ expected: expected_status,
+ allowSilentFail: allow_silent_fail,
+ prettyPath: prettyPath,
+ minAsserts: minAsserts,
+ maxAsserts: maxAsserts,
+ needsFocus: needs_focus,
+ slow: slow,
+ prefSettings1: testPrefSettings,
+ prefSettings2: refPrefSettings,
+ fuzzyMaxDelta: fuzzy_max_delta,
+ fuzzyMaxPixels: fuzzy_max_pixels,
+ url1: testURI,
+ url2: null });
+ } else if (items[0] == TYPE_REFTEST_EQUAL || items[0] == TYPE_REFTEST_NOTEQUAL) {
+ if (items.length != 3)
+ throw "Error 5 in manifest file " + aURL.spec + " line " + lineNo;
+ var [testURI, refURI] = runHttp
+ ? ServeFiles(principal, httpDepth,
+ listURL, [items[1], items[2]])
+ : [gIOService.newURI(items[1], null, listURL),
+ gIOService.newURI(items[2], null, listURL)];
+ var prettyPath = runHttp
+ ? gIOService.newURI(items[1], null, listURL).spec
+ : testURI.spec;
+ secMan.checkLoadURIWithPrincipal(principal, testURI,
+ CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ secMan.checkLoadURIWithPrincipal(principal, refURI,
+ CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ AddTestItem({ type: items[0],
+ expected: expected_status,
+ allowSilentFail: allow_silent_fail,
+ prettyPath: prettyPath,
+ minAsserts: minAsserts,
+ maxAsserts: maxAsserts,
+ needsFocus: needs_focus,
+ slow: slow,
+ prefSettings1: testPrefSettings,
+ prefSettings2: refPrefSettings,
+ fuzzyMaxDelta: fuzzy_max_delta,
+ fuzzyMaxPixels: fuzzy_max_pixels,
+ url1: testURI,
+ url2: refURI });
+ } else {
+ throw "Error 6 in manifest file " + aURL.spec + " line " + lineNo;
+ }
+ }
+}
+
+function AddURIUseCount(uri)
+{
+ if (uri == null)
+ return;
+
+ var spec = uri.spec;
+ if (spec in gURIUseCounts) {
+ gURIUseCounts[spec]++;
+ } else {
+ gURIUseCounts[spec] = 1;
+ }
+}
+
+function BuildUseCounts()
+{
+ gURIUseCounts = {};
+ for (var i = 0; i < gURLs.length; ++i) {
+ var url = gURLs[i];
+ if (url.expected != EXPECTED_DEATH &&
+ (url.type == TYPE_REFTEST_EQUAL ||
+ url.type == TYPE_REFTEST_NOTEQUAL)) {
+ if (url.prefSettings1.length == 0) {
+ AddURIUseCount(gURLs[i].url1);
+ }
+ if (url.prefSettings2.length == 0) {
+ AddURIUseCount(gURLs[i].url2);
+ }
+ }
+ }
+}
+
+function ServeFiles(manifestPrincipal, depth, aURL, files)
+{
+ var listURL = aURL.QueryInterface(CI.nsIFileURL);
+ var directory = listURL.file.parent;
+
+ // Allow serving a tree that's an ancestor of the directory containing
+ // the files so that they can use resources in ../ (etc.).
+ var dirPath = "/";
+ while (depth > 0) {
+ dirPath = "/" + directory.leafName + dirPath;
+ directory = directory.parent;
+ --depth;
+ }
+
+ gCount++;
+ var path = "/" + Date.now() + "/" + gCount;
+ gServer.registerDirectory(path + "/", directory);
+
+ var secMan = CC[NS_SCRIPTSECURITYMANAGER_CONTRACTID]
+ .getService(CI.nsIScriptSecurityManager);
+
+ var testbase = gIOService.newURI("http://localhost:" + gHttpServerPort +
+ path + dirPath, null, null);
+
+ function FileToURI(file)
+ {
+ // Only serve relative URIs via the HTTP server, not absolute
+ // ones like about:blank.
+ var testURI = gIOService.newURI(file, null, testbase);
+
+ // XXX necessary? manifestURL guaranteed to be file, others always HTTP
+ secMan.checkLoadURIWithPrincipal(manifestPrincipal, testURI,
+ CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ return testURI;
+ }
+
+ return files.map(FileToURI);
+}
+
+// Return true iff this window is focused when this function returns.
+function Focus()
+{
+ var fm = CC["@mozilla.org/focus-manager;1"].getService(CI.nsIFocusManager);
+ fm.focusedWindow = gContainingWindow;
+#ifdef XP_MACOSX
+ try {
+ var dock = CC["@mozilla.org/widget/macdocksupport;1"].getService(CI.nsIMacDockSupport);
+ dock.activateApplication(true);
+ } catch(ex) {
+ }
+#endif // XP_MACOSX
+ return true;
+}
+
+function StartCurrentTest()
+{
+ gTestLog = [];
+
+ // make sure we don't run tests that are expected to kill the browser
+ while (gURLs.length > 0) {
+ var test = gURLs[0];
+ if (test.expected == EXPECTED_DEATH) {
+ ++gTestResults.Skip;
+ gDumpLog("REFTEST TEST-KNOWN-FAIL | " + test.url1.spec + " | (SKIP)\n");
+ gURLs.shift();
+ } else if (test.needsFocus && !Focus()) {
+ // FIXME: Marking this as a known fail is dangerous! What
+ // if it starts failing all the time?
+ ++gTestResults.Skip;
+ gDumpLog("REFTEST TEST-KNOWN-FAIL | " + test.url1.spec + " | (SKIPPED; COULDN'T GET FOCUS)\n");
+ gURLs.shift();
+ } else if (test.slow && !gRunSlowTests) {
+ ++gTestResults.Slow;
+ gDumpLog("REFTEST TEST-KNOWN-SLOW | " + test.url1.spec + " | (SLOW)\n");
+ gURLs.shift();
+ } else {
+ break;
+ }
+ }
+
+ if (gURLs.length == 0) {
+ RestoreChangedPreferences();
+ DoneTests();
+ }
+ else {
+ gDumpLog("REFTEST TEST-START | " + gURLs[0].prettyPath + "\n");
+ var currentTest = gTotalTests - gURLs.length;
+ gContainingWindow.document.title = "reftest: " + currentTest + " / " + gTotalTests +
+ " (" + Math.floor(100 * (currentTest / gTotalTests)) + "%)";
+ StartCurrentURI(1);
+ }
+}
+
+function StartCurrentURI(aState)
+{
+ gState = aState;
+ gCurrentURL = gURLs[0]["url" + aState].spec;
+
+ RestoreChangedPreferences();
+
+ var prefSettings = gURLs[0]["prefSettings" + aState];
+ if (prefSettings.length > 0) {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ var badPref = undefined;
+ try {
+ prefSettings.forEach(function(ps) {
+ var oldVal;
+ if (ps.type == PREF_BOOLEAN) {
+ try {
+ oldVal = prefs.getBoolPref(ps.name);
+ } catch (e) {
+ badPref = "boolean preference '" + ps.name + "'";
+ throw "bad pref";
+ }
+ } else if (ps.type == PREF_STRING) {
+ try {
+ oldVal = prefs.getCharPref(ps.name);
+ } catch (e) {
+ badPref = "string preference '" + ps.name + "'";
+ throw "bad pref";
+ }
+ } else if (ps.type == PREF_INTEGER) {
+ try {
+ oldVal = prefs.getIntPref(ps.name);
+ } catch (e) {
+ badPref = "integer preference '" + ps.name + "'";
+ throw "bad pref";
+ }
+ } else {
+ throw "internal error - unknown preference type";
+ }
+ if (oldVal != ps.value) {
+ gPrefsToRestore.push( { name: ps.name,
+ type: ps.type,
+ value: oldVal } );
+ var value = ps.value;
+ if (ps.type == PREF_BOOLEAN) {
+ prefs.setBoolPref(ps.name, value);
+ } else if (ps.type == PREF_STRING) {
+ prefs.setCharPref(ps.name, value);
+ value = '"' + value + '"';
+ } else if (ps.type == PREF_INTEGER) {
+ prefs.setIntPref(ps.name, value);
+ }
+ gDumpLog("SET PREFERENCE pref(" + ps.name + "," + value + ")\n");
+ }
+ });
+ } catch (e) {
+ if (e == "bad pref") {
+ var test = gURLs[0];
+ if (test.expected == EXPECTED_FAIL) {
+ gDumpLog("REFTEST TEST-KNOWN-FAIL | " + test.url1.spec +
+ " | (SKIPPED; " + badPref + " not known or wrong type)\n");
+ ++gTestResults.Skip;
+ } else {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + test.url1.spec +
+ " | " + badPref + " not known or wrong type\n");
+ ++gTestResults.UnexpectedFail;
+ }
+ } else {
+ throw e;
+ }
+ }
+ if (badPref != undefined) {
+ // skip the test that had a bad preference
+ gURLs.shift();
+
+ StartCurrentTest();
+ return;
+ }
+ }
+
+ if (prefSettings.length == 0 &&
+ gURICanvases[gCurrentURL] &&
+ (gURLs[0].type == TYPE_REFTEST_EQUAL ||
+ gURLs[0].type == TYPE_REFTEST_NOTEQUAL) &&
+ gURLs[0].maxAsserts == 0) {
+ // Pretend the document loaded --- RecordResult will notice
+ // there's already a canvas for this URL
+ gContainingWindow.setTimeout(RecordResult, 0);
+ } else {
+ var currentTest = gTotalTests - gURLs.length;
+ gDumpLog("REFTEST TEST-LOAD | " + gCurrentURL + " | " + currentTest + " / " + gTotalTests +
+ " (" + Math.floor(100 * (currentTest / gTotalTests)) + "%)\n");
+ LogInfo("START " + gCurrentURL);
+ var type = gURLs[0].type
+ if (TYPE_SCRIPT == type) {
+ SendLoadScriptTest(gCurrentURL, gLoadTimeout);
+ } else {
+ SendLoadTest(type, gCurrentURL, gLoadTimeout);
+ }
+ }
+}
+
+function DoneTests()
+{
+ gDumpLog("REFTEST FINISHED: Slowest test took " + gSlowestTestTime +
+ "ms (" + gSlowestTestURL + ")\n");
+
+ gDumpLog("REFTEST INFO | Result summary:\n");
+ var count = gTestResults.Pass + gTestResults.LoadOnly;
+ gDumpLog("REFTEST INFO | Successful: " + count + " (" +
+ gTestResults.Pass + " pass, " +
+ gTestResults.LoadOnly + " load only)\n");
+ count = gTestResults.Exception + gTestResults.FailedLoad +
+ gTestResults.UnexpectedFail + gTestResults.UnexpectedPass +
+ gTestResults.AssertionUnexpected +
+ gTestResults.AssertionUnexpectedFixed;
+ gDumpLog("REFTEST INFO | Unexpected: " + count + " (" +
+ gTestResults.UnexpectedFail + " unexpected fail, " +
+ gTestResults.UnexpectedPass + " unexpected pass, " +
+ gTestResults.AssertionUnexpected + " unexpected asserts, " +
+ gTestResults.AssertionUnexpectedFixed + " unexpected fixed asserts, " +
+ gTestResults.FailedLoad + " failed load, " +
+ gTestResults.Exception + " exception)\n");
+ count = gTestResults.KnownFail + gTestResults.AssertionKnown +
+ gTestResults.Random + gTestResults.Skip + gTestResults.Slow;
+ gDumpLog("REFTEST INFO | Known problems: " + count + " (" +
+ gTestResults.KnownFail + " known fail, " +
+ gTestResults.AssertionKnown + " known asserts, " +
+ gTestResults.Random + " random, " +
+ gTestResults.Skip + " skipped, " +
+ gTestResults.Slow + " slow)\n");
+
+ gDumpLog("REFTEST INFO | Total canvas count = " + gRecycledCanvases.length + "\n");
+
+ gDumpLog("REFTEST TEST-START | Shutdown\n");
+ function onStopped() {
+ let appStartup = CC["@mozilla.org/toolkit/app-startup;1"].getService(CI.nsIAppStartup);
+ appStartup.quit(CI.nsIAppStartup.eForceQuit);
+ }
+ if (gServer) {
+ gServer.stop(onStopped);
+ }
+ else {
+ onStopped();
+ }
+}
+
+function UpdateCanvasCache(url, canvas)
+{
+ var spec = url.spec;
+
+ --gURIUseCounts[spec];
+
+ if (gNoCanvasCache || gURIUseCounts[spec] == 0) {
+ ReleaseCanvas(canvas);
+ delete gURICanvases[spec];
+ } else if (gURIUseCounts[spec] > 0) {
+ gURICanvases[spec] = canvas;
+ } else {
+ throw "Use counts were computed incorrectly";
+ }
+}
+
+// Recompute drawWindow flags for every drawWindow operation.
+// We have to do this every time since our window can be
+// asynchronously resized (e.g. by the window manager, to make
+// it fit on screen) at unpredictable times.
+// Fortunately this is pretty cheap.
+function DoDrawWindow(ctx, x, y, w, h)
+{
+ var flags = ctx.DRAWWINDOW_DRAW_CARET | ctx.DRAWWINDOW_DRAW_VIEW;
+ var testRect = gBrowser.getBoundingClientRect();
+ if (gIgnoreWindowSize ||
+ (0 <= testRect.left &&
+ 0 <= testRect.top &&
+ gContainingWindow.innerWidth >= testRect.right &&
+ gContainingWindow.innerHeight >= testRect.bottom)) {
+ // We can use the window's retained layer manager
+ // because the window is big enough to display the entire
+ // browser element
+ flags |= ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
+ } else if (gBrowserIsRemote) {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gCurrentURL + " | can't drawWindow remote content\n");
+ ++gTestResults.Exception;
+ }
+
+ if (gDrawWindowFlags != flags) {
+ // Every time the flags change, dump the new state.
+ gDrawWindowFlags = flags;
+ var flagsStr = "DRAWWINDOW_DRAW_CARET | DRAWWINDOW_DRAW_VIEW";
+ if (flags & ctx.DRAWWINDOW_USE_WIDGET_LAYERS) {
+ flagsStr += " | DRAWWINDOW_USE_WIDGET_LAYERS";
+ } else {
+ // Output a special warning because we need to be able to detect
+ // this whenever it happens.
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | WARNING: USE_WIDGET_LAYERS disabled\n");
+ }
+ gDumpLog("REFTEST INFO | drawWindow flags = " + flagsStr +
+ "; window size = " + gContainingWindow.innerWidth + "," + gContainingWindow.innerHeight +
+ "; test browser size = " + testRect.width + "," + testRect.height +
+ "\n");
+ }
+
+ LogInfo("DoDrawWindow " + x + "," + y + "," + w + "," + h);
+ ctx.drawWindow(gContainingWindow, x, y, w, h, "rgb(255,255,255)",
+ gDrawWindowFlags);
+}
+
+function InitCurrentCanvasWithSnapshot()
+{
+ LogInfo("Initializing canvas snapshot");
+
+ if (gURLs[0].type == TYPE_LOAD || gURLs[0].type == TYPE_SCRIPT) {
+ // We don't want to snapshot this kind of test
+ return false;
+ }
+
+ if (!gCurrentCanvas) {
+ gCurrentCanvas = AllocateCanvas();
+ }
+
+ var ctx = gCurrentCanvas.getContext("2d");
+ DoDrawWindow(ctx, 0, 0, gCurrentCanvas.width, gCurrentCanvas.height);
+ return true;
+}
+
+function UpdateCurrentCanvasForInvalidation(rects)
+{
+ LogInfo("Updating canvas for invalidation");
+
+ if (!gCurrentCanvas) {
+ return;
+ }
+
+ var ctx = gCurrentCanvas.getContext("2d");
+ for (var i = 0; i < rects.length; ++i) {
+ var r = rects[i];
+ // Set left/top/right/bottom to pixel boundaries
+ var left = Math.floor(r.left);
+ var top = Math.floor(r.top);
+ var right = Math.ceil(r.right);
+ var bottom = Math.ceil(r.bottom);
+
+ ctx.save();
+ ctx.translate(left, top);
+ DoDrawWindow(ctx, left, top, right - left, bottom - top);
+ ctx.restore();
+ }
+}
+
+function UpdateWholeCurrentCanvasForInvalidation()
+{
+ LogInfo("Updating entire canvas for invalidation");
+
+ if (!gCurrentCanvas) {
+ return;
+ }
+
+ var ctx = gCurrentCanvas.getContext("2d");
+ DoDrawWindow(ctx, 0, 0, gCurrentCanvas.width, gCurrentCanvas.height);
+}
+
+function RecordResult(testRunTime, errorMsg, scriptResults)
+{
+ LogInfo("RecordResult fired");
+
+ // Keep track of which test was slowest, and how long it took.
+ if (testRunTime > gSlowestTestTime) {
+ gSlowestTestTime = testRunTime;
+ gSlowestTestURL = gCurrentURL;
+ }
+
+ // Not 'const ...' because of 'EXPECTED_*' value dependency.
+ var outputs = {};
+ const randomMsg = "(EXPECTED RANDOM)";
+ outputs[EXPECTED_PASS] = {
+ true: {s: "TEST-PASS" , n: "Pass"},
+ false: {s: "TEST-UNEXPECTED-FAIL" , n: "UnexpectedFail"}
+ };
+ outputs[EXPECTED_FAIL] = {
+ true: {s: "TEST-UNEXPECTED-PASS" , n: "UnexpectedPass"},
+ false: {s: "TEST-KNOWN-FAIL" , n: "KnownFail"}
+ };
+ outputs[EXPECTED_RANDOM] = {
+ true: {s: "TEST-PASS" + randomMsg , n: "Random"},
+ false: {s: "TEST-KNOWN-FAIL" + randomMsg, n: "Random"}
+ };
+ outputs[EXPECTED_FUZZY] = outputs[EXPECTED_PASS];
+
+ var output;
+
+ if (gURLs[0].type == TYPE_LOAD) {
+ ++gTestResults.LoadOnly;
+ gDumpLog("REFTEST TEST-PASS | " + gURLs[0].prettyPath + " | (LOAD ONLY)\n");
+ gCurrentCanvas = null;
+ FinishTestItem();
+ return;
+ }
+ if (gURLs[0].type == TYPE_SCRIPT) {
+ var expected = gURLs[0].expected;
+
+ if (errorMsg) {
+ // Force an unexpected failure to alert the test author to fix the test.
+ expected = EXPECTED_PASS;
+ } else if (scriptResults.length == 0) {
+ // This failure may be due to a JavaScript Engine bug causing
+ // early termination of the test. If we do not allow silent
+ // failure, report an error.
+ if (!gURLs[0].allowSilentFail)
+ errorMsg = "No test results reported. (SCRIPT)\n";
+ else
+ gDumpLog("REFTEST INFO | An expected silent failure occurred \n");
+ }
+
+ if (errorMsg) {
+ output = outputs[expected][false];
+ ++gTestResults[output.n];
+ var result = "REFTEST " + output.s + " | " +
+ gURLs[0].prettyPath + " | " + // the URL being tested
+ errorMsg;
+
+ gDumpLog(result);
+ FinishTestItem();
+ return;
+ }
+
+ var anyFailed = scriptResults.some(function(result) { return !result.passed; });
+ var outputPair;
+ if (anyFailed && expected == EXPECTED_FAIL) {
+ // If we're marked as expected to fail, and some (but not all) tests
+ // passed, treat those tests as though they were marked random
+ // (since we can't tell whether they were really intended to be
+ // marked failing or not).
+ outputPair = { true: outputs[EXPECTED_RANDOM][true],
+ false: outputs[expected][false] };
+ } else {
+ outputPair = outputs[expected];
+ }
+ var index = 0;
+ scriptResults.forEach(function(result) {
+ var output = outputPair[result.passed];
+
+ ++gTestResults[output.n];
+ result = "REFTEST " + output.s + " | " +
+ gURLs[0].prettyPath + " | " + // the URL being tested
+ result.description + " item " + (++index) + "\n";
+ gDumpLog(result);
+ });
+
+ if (anyFailed && expected == EXPECTED_PASS) {
+ FlushTestLog();
+ }
+
+ FinishTestItem();
+ return;
+ }
+
+ if (gURLs[0]["prefSettings" + gState].length == 0 &&
+ gURICanvases[gCurrentURL]) {
+ gCurrentCanvas = gURICanvases[gCurrentURL];
+ }
+ if (gCurrentCanvas == null) {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gCurrentURL + " | program error managing snapshots\n");
+ ++gTestResults.Exception;
+ }
+ if (gState == 1) {
+ gCanvas1 = gCurrentCanvas;
+ } else {
+ gCanvas2 = gCurrentCanvas;
+ }
+ gCurrentCanvas = null;
+
+ ResetRenderingState();
+
+ switch (gState) {
+ case 1:
+ // First document has been loaded.
+ // Proceed to load the second document.
+
+ CleanUpCrashDumpFiles();
+ StartCurrentURI(2);
+ break;
+ case 2:
+ // Both documents have been loaded. Compare the renderings and see
+ // if the comparison result matches the expected result specified
+ // in the manifest.
+
+ // number of different pixels
+ var differences;
+ // whether the two renderings match:
+ var equal;
+ var maxDifference = {};
+
+ differences = gWindowUtils.compareCanvases(gCanvas1, gCanvas2, maxDifference);
+ equal = (differences == 0);
+
+ // what is expected on this platform (PASS, FAIL, or RANDOM)
+ var expected = gURLs[0].expected;
+
+ if (maxDifference.value > 0 && maxDifference.value <= gURLs[0].fuzzyMaxDelta &&
+ differences <= gURLs[0].fuzzyMaxPixels) {
+ if (equal) {
+ throw "Inconsistent result from compareCanvases.";
+ }
+ equal = expected == EXPECTED_FUZZY;
+ gDumpLog("REFTEST fuzzy match\n");
+ }
+
+ // whether the comparison result matches what is in the manifest
+ var test_passed = (equal == (gURLs[0].type == TYPE_REFTEST_EQUAL)) && !gFailedNoPaint;
+
+ output = outputs[expected][test_passed];
+
+ ++gTestResults[output.n];
+
+ // It's possible that we failed both reftest-no-paint and the normal comparison, but we don't
+ // have a way to annotate these separately, so just print an error for the no-paint failure.
+ if (gFailedNoPaint) {
+ if (expected == EXPECTED_FAIL) {
+ gDumpLog("REFTEST TEST-KNOWN-FAIL | " + gURLs[0].prettyPath + " | failed reftest-no-paint\n");
+ } else {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gURLs[0].prettyPath + " | failed reftest-no-paint\n");
+ }
+ } else {
+ var result = "REFTEST " + output.s + " | " +
+ gURLs[0].prettyPath + " | "; // the URL being tested
+ switch (gURLs[0].type) {
+ case TYPE_REFTEST_NOTEQUAL:
+ result += "image comparison (!=)";
+ break;
+ case TYPE_REFTEST_EQUAL:
+ result += "image comparison (==)";
+ break;
+ }
+
+ if (!test_passed && expected == EXPECTED_PASS ||
+ !test_passed && expected == EXPECTED_FUZZY ||
+ test_passed && expected == EXPECTED_FAIL) {
+ if (!equal) {
+ result += ", max difference: " + maxDifference.value + ", number of differing pixels: " + differences + "\n";
+ result += "REFTEST IMAGE 1 (TEST): " + gCanvas1.toDataURL() + "\n";
+ result += "REFTEST IMAGE 2 (REFERENCE): " + gCanvas2.toDataURL() + "\n";
+ } else {
+ result += "\n";
+ gDumpLog("REFTEST IMAGE: " + gCanvas1.toDataURL() + "\n");
+ }
+ } else {
+ result += "\n";
+ }
+
+ gDumpLog(result);
+ }
+
+ if (!test_passed && expected == EXPECTED_PASS) {
+ FlushTestLog();
+ }
+
+ if (gURLs[0].prefSettings1.length == 0) {
+ UpdateCanvasCache(gURLs[0].url1, gCanvas1);
+ }
+ if (gURLs[0].prefSettings2.length == 0) {
+ UpdateCanvasCache(gURLs[0].url2, gCanvas2);
+ }
+
+ CleanUpCrashDumpFiles();
+ FinishTestItem();
+ break;
+ default:
+ throw "Unexpected state.";
+ }
+}
+
+function LoadFailed(why)
+{
+ ++gTestResults.FailedLoad;
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " +
+ gURLs[0]["url" + gState].spec + " | load failed: " + why + "\n");
+ FlushTestLog();
+ FinishTestItem();
+}
+
+function RemoveExpectedCrashDumpFiles()
+{
+ if (gExpectingProcessCrash) {
+ for each (let crashFilename in gExpectedCrashDumpFiles) {
+ let file = gCrashDumpDir.clone();
+ file.append(crashFilename);
+ if (file.exists()) {
+ file.remove(false);
+ }
+ }
+ }
+ gExpectedCrashDumpFiles.length = 0;
+}
+
+function FindUnexpectedCrashDumpFiles()
+{
+ if (!gCrashDumpDir.exists()) {
+ return;
+ }
+
+ let entries = gCrashDumpDir.directoryEntries;
+ if (!entries) {
+ return;
+ }
+
+ let foundCrashDumpFile = false;
+ while (entries.hasMoreElements()) {
+ let file = entries.getNext().QueryInterface(CI.nsIFile);
+ let path = String(file.path);
+ if (path.match(/\.(dmp|extra)$/) && !gUnexpectedCrashDumpFiles[path]) {
+ if (!foundCrashDumpFile) {
+ ++gTestResults.UnexpectedFail;
+ foundCrashDumpFile = true;
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gCurrentURL +
+ " | This test left crash dumps behind, but we weren't expecting it to!\n");
+ }
+ gDumpLog("REFTEST INFO | Found unexpected crash dump file " + path +
+ ".\n");
+ gUnexpectedCrashDumpFiles[path] = true;
+ }
+ }
+}
+
+function CleanUpCrashDumpFiles()
+{
+ RemoveExpectedCrashDumpFiles();
+ FindUnexpectedCrashDumpFiles();
+ gExpectingProcessCrash = false;
+}
+
+function FinishTestItem()
+{
+ // Replace document with BLANK_URL_FOR_CLEARING in case there are
+ // assertions when unloading.
+ gDumpLog("REFTEST INFO | Loading a blank page\n");
+ // After clearing, content will notify us of the assertion count
+ // and tests will continue.
+ SetAsyncScroll(false);
+ SendClear();
+ gFailedNoPaint = false;
+}
+
+function DoAssertionCheck(numAsserts)
+{
+ if (gDebug.isDebugBuild) {
+ if (gBrowserIsRemote) {
+ // Count chrome-process asserts too when content is out of
+ // process.
+ var newAssertionCount = gDebug.assertionCount;
+ var numLocalAsserts = newAssertionCount - gAssertionCount;
+ gAssertionCount = newAssertionCount;
+
+ numAsserts += numLocalAsserts;
+ }
+
+ var minAsserts = gURLs[0].minAsserts;
+ var maxAsserts = gURLs[0].maxAsserts;
+
+ var expectedAssertions = "expected " + minAsserts;
+ if (minAsserts != maxAsserts) {
+ expectedAssertions += " to " + maxAsserts;
+ }
+ expectedAssertions += " assertions";
+
+ if (numAsserts < minAsserts) {
+ ++gTestResults.AssertionUnexpectedFixed;
+ gDumpLog("REFTEST TEST-UNEXPECTED-PASS | " + gURLs[0].prettyPath +
+ " | assertion count " + numAsserts + " is less than " +
+ expectedAssertions + "\n");
+ } else if (numAsserts > maxAsserts) {
+ ++gTestResults.AssertionUnexpected;
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gURLs[0].prettyPath +
+ " | assertion count " + numAsserts + " is more than " +
+ expectedAssertions + "\n");
+ } else if (numAsserts != 0) {
+ ++gTestResults.AssertionKnown;
+ gDumpLog("REFTEST TEST-KNOWN-FAIL | " + gURLs[0].prettyPath +
+ " | assertion count " + numAsserts + " matches " +
+ expectedAssertions + "\n");
+ }
+ }
+
+ gDumpLog("REFTEST TEST-END | " + gURLs[0].prettyPath + "\n");
+
+ // And start the next test.
+ gURLs.shift();
+ StartCurrentTest();
+}
+
+function ResetRenderingState()
+{
+ SendResetRenderingState();
+ // We would want to clear any viewconfig here, if we add support for it
+}
+
+function RestoreChangedPreferences()
+{
+ if (gPrefsToRestore.length > 0) {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ gPrefsToRestore.reverse();
+ gPrefsToRestore.forEach(function(ps) {
+ var value = ps.value;
+ if (ps.type == PREF_BOOLEAN) {
+ prefs.setBoolPref(ps.name, value);
+ } else if (ps.type == PREF_STRING) {
+ prefs.setCharPref(ps.name, value);
+ value = '"' + value + '"';
+ } else if (ps.type == PREF_INTEGER) {
+ prefs.setIntPref(ps.name, value);
+ }
+ gDumpLog("RESTORE PREFERENCE pref(" + ps.name + "," + value + ")\n");
+ });
+ gPrefsToRestore = [];
+ }
+}
+
+function RegisterMessageListenersAndLoadContentScript()
+{
+ gBrowserMessageManager.addMessageListener(
+ "reftest:AssertionCount",
+ function (m) { RecvAssertionCount(m.json.count); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:ContentReady",
+ function (m) { return RecvContentReady() }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:Exception",
+ function (m) { RecvException(m.json.what) }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:FailedLoad",
+ function (m) { RecvFailedLoad(m.json.why); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:FailedNoPaint",
+ function (m) { RecvFailedNoPaint(); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:InitCanvasWithSnapshot",
+ function (m) { return RecvInitCanvasWithSnapshot(); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:Log",
+ function (m) { RecvLog(m.json.type, m.json.msg); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:ScriptResults",
+ function (m) { RecvScriptResults(m.json.runtimeMs, m.json.error, m.json.results); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:TestDone",
+ function (m) { RecvTestDone(m.json.runtimeMs); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:UpdateCanvasForInvalidation",
+ function (m) { RecvUpdateCanvasForInvalidation(m.json.rects); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:UpdateWholeCanvasForInvalidation",
+ function (m) { RecvUpdateWholeCanvasForInvalidation(); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:ExpectProcessCrash",
+ function (m) { RecvExpectProcessCrash(); }
+ );
+ gBrowserMessageManager.addMessageListener(
+ "reftest:EnableAsyncScroll",
+ function (m) { SetAsyncScroll(true); }
+ );
+
+ gBrowserMessageManager.loadFrameScript("chrome://reftest/content/reftest-content.js", true);
+}
+
+function SetAsyncScroll(enabled)
+{
+ gBrowser.QueryInterface(CI.nsIFrameLoaderOwner).frameLoader.renderMode =
+ enabled ? CI.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL :
+ CI.nsIFrameLoader.RENDER_MODE_DEFAULT;
+}
+
+function RecvAssertionCount(count)
+{
+ DoAssertionCheck(count);
+}
+
+function RecvContentReady()
+{
+ InitAndStartRefTests();
+ return { remote: gBrowserIsRemote };
+}
+
+function RecvException(what)
+{
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gCurrentURL + " | " + what + "\n");
+ ++gTestResults.Exception;
+}
+
+function RecvFailedLoad(why)
+{
+ LoadFailed(why);
+}
+
+function RecvFailedNoPaint()
+{
+ gFailedNoPaint = true;
+}
+
+function RecvInitCanvasWithSnapshot()
+{
+ var painted = InitCurrentCanvasWithSnapshot();
+ return { painted: painted };
+}
+
+function RecvLog(type, msg)
+{
+ msg = "[CONTENT] "+ msg;
+ if (type == "info") {
+ LogInfo(msg);
+ } else if (type == "warning") {
+ LogWarning(msg);
+ } else {
+ gDumpLog("REFTEST TEST-UNEXPECTED-FAIL | " + gCurrentURL + " | unknown log type " + type + "\n");
+ ++gTestResults.Exception;
+ }
+}
+
+function RecvScriptResults(runtimeMs, error, results)
+{
+ RecordResult(runtimeMs, error, results);
+}
+
+function RecvTestDone(runtimeMs)
+{
+ RecordResult(runtimeMs, '', [ ]);
+}
+
+function RecvUpdateCanvasForInvalidation(rects)
+{
+ UpdateCurrentCanvasForInvalidation(rects);
+}
+
+function RecvUpdateWholeCanvasForInvalidation()
+{
+ UpdateWholeCurrentCanvasForInvalidation();
+}
+
+function OnProcessCrashed(subject, topic, data)
+{
+ var id;
+ subject = subject.QueryInterface(CI.nsIPropertyBag2);
+ if (topic == "plugin-crashed") {
+ id = subject.getPropertyAsAString("pluginDumpID");
+ } else if (topic == "ipc:content-shutdown") {
+ id = subject.getPropertyAsAString("dumpID");
+ }
+ if (id) {
+ gExpectedCrashDumpFiles.push(id + ".dmp");
+ gExpectedCrashDumpFiles.push(id + ".extra");
+ }
+}
+
+function RegisterProcessCrashObservers()
+{
+ var os = CC[NS_OBSERVER_SERVICE_CONTRACTID]
+ .getService(CI.nsIObserverService);
+ os.addObserver(OnProcessCrashed, "plugin-crashed", false);
+ os.addObserver(OnProcessCrashed, "ipc:content-shutdown", false);
+}
+
+function RecvExpectProcessCrash()
+{
+ gExpectingProcessCrash = true;
+}
+
+function SendClear()
+{
+ gBrowserMessageManager.sendAsyncMessage("reftest:Clear");
+}
+
+function SendLoadScriptTest(uri, timeout)
+{
+ gBrowserMessageManager.sendAsyncMessage("reftest:LoadScriptTest",
+ { uri: uri, timeout: timeout });
+}
+
+function SendLoadTest(type, uri, timeout)
+{
+ gBrowserMessageManager.sendAsyncMessage("reftest:LoadTest",
+ { type: type, uri: uri, timeout: timeout }
+ );
+}
+
+function SendResetRenderingState()
+{
+ gBrowserMessageManager.sendAsyncMessage("reftest:ResetRenderingState");
+}
diff --git a/layout/tools/reftest/reftest.xul b/layout/tools/reftest/reftest.xul
new file mode 100644
index 000000000..50580bfed
--- /dev/null
+++ b/layout/tools/reftest/reftest.xul
@@ -0,0 +1,22 @@
+<!-- vim: set shiftwidth=4 tabstop=8 autoindent expandtab: -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet type="text/css" href="data:text/css,
+
+%23_box_windowsDefaultTheme:-moz-system-metric(windows-default-theme) {
+ display: none;
+}
+
+" ?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="reftest-window"
+ hidechrome="true"
+ onload="OnRefTestLoad();"
+ onunload="OnRefTestUnload();"
+ style="background:white; overflow:hidden"
+ >
+ <script type="application/ecmascript" src="reftest.js" />
+ <!-- The reftest browser element is dynamically created, here -->
+</window>
diff --git a/layout/tools/reftest/remotereftest.py b/layout/tools/reftest/remotereftest.py
new file mode 100644
index 000000000..eae0afd89
--- /dev/null
+++ b/layout/tools/reftest/remotereftest.py
@@ -0,0 +1,517 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+import os
+import time
+import tempfile
+import traceback
+
+# We need to know our current directory so that we can serve our test files from it.
+SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
+
+from runreftest import RefTest
+from runreftest import ReftestOptions
+from automation import Automation
+import devicemanager
+import droid
+from remoteautomation import RemoteAutomation, fennecLogcatFilters
+
+class RemoteOptions(ReftestOptions):
+ def __init__(self, automation):
+ ReftestOptions.__init__(self, automation)
+
+ defaults = {}
+ defaults["logFile"] = "reftest.log"
+ # app, xrePath and utilityPath variables are set in main function
+ defaults["app"] = ""
+ defaults["xrePath"] = ""
+ defaults["utilityPath"] = ""
+
+ self.add_option("--remote-app-path", action="store",
+ type = "string", dest = "remoteAppPath",
+ help = "Path to remote executable relative to device root using only forward slashes. Either this or app must be specified, but not both.")
+ defaults["remoteAppPath"] = None
+
+ self.add_option("--deviceIP", action="store",
+ type = "string", dest = "deviceIP",
+ help = "ip address of remote device to test")
+ defaults["deviceIP"] = None
+
+ self.add_option("--devicePort", action="store",
+ type = "string", dest = "devicePort",
+ help = "port of remote device to test")
+ defaults["devicePort"] = 20701
+
+ self.add_option("--remote-product-name", action="store",
+ type = "string", dest = "remoteProductName",
+ help = "Name of product to test - either fennec or firefox, defaults to fennec")
+ defaults["remoteProductName"] = "fennec"
+
+ self.add_option("--remote-webserver", action="store",
+ type = "string", dest = "remoteWebServer",
+ help = "IP Address of the webserver hosting the reftest content")
+ defaults["remoteWebServer"] = automation.getLanIp()
+
+ self.add_option("--http-port", action = "store",
+ type = "string", dest = "httpPort",
+ help = "port of the web server for http traffic")
+ defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
+
+ self.add_option("--ssl-port", action = "store",
+ type = "string", dest = "sslPort",
+ help = "Port for https traffic to the web server")
+ defaults["sslPort"] = automation.DEFAULT_SSL_PORT
+
+ self.add_option("--remote-logfile", action="store",
+ type = "string", dest = "remoteLogFile",
+ help = "Name of log file on the device relative to device root. PLEASE USE ONLY A FILENAME.")
+ defaults["remoteLogFile"] = None
+
+ self.add_option("--enable-privilege", action="store_true", dest = "enablePrivilege",
+ help = "add webserver and port to the user.js file for remote script access and universalXPConnect")
+ defaults["enablePrivilege"] = False
+
+ self.add_option("--pidfile", action = "store",
+ type = "string", dest = "pidFile",
+ help = "name of the pidfile to generate")
+ defaults["pidFile"] = ""
+
+ self.add_option("--bootstrap", action="store_true", dest = "bootstrap",
+ help = "test with a bootstrap addon required for native Fennec")
+ defaults["bootstrap"] = False
+
+ self.add_option("--dm_trans", action="store",
+ type = "string", dest = "dm_trans",
+ help = "the transport to use to communicate with device: [adb|sut]; default=sut")
+ defaults["dm_trans"] = "sut"
+
+ self.add_option("--remoteTestRoot", action = "store",
+ type = "string", dest = "remoteTestRoot",
+ help = "remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
+ defaults["remoteTestRoot"] = None
+
+ self.add_option("--httpd-path", action = "store",
+ type = "string", dest = "httpdPath",
+ help = "path to the httpd.js file")
+ defaults["httpdPath"] = None
+
+ defaults["localLogName"] = None
+
+ self.set_defaults(**defaults)
+
+ def verifyRemoteOptions(self, options):
+ # Ensure our defaults are set properly for everything we can infer
+ if not options.remoteTestRoot:
+ options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + '/reftest'
+ options.remoteProfile = options.remoteTestRoot + "/profile"
+
+ # Verify that our remotewebserver is set properly
+ if (options.remoteWebServer == None or
+ options.remoteWebServer == '127.0.0.1'):
+ print "ERROR: Either you specified the loopback for the remote webserver or ",
+ print "your local IP cannot be detected. Please provide the local ip in --remote-webserver"
+ return None
+
+ # One of remoteAppPath (relative path to application) or the app (executable) must be
+ # set, but not both. If both are set, we destroy the user's selection for app
+ # so instead of silently destroying a user specificied setting, we error.
+ if (options.remoteAppPath and options.app):
+ print "ERROR: You cannot specify both the remoteAppPath and the app"
+ return None
+ elif (options.remoteAppPath):
+ options.app = options.remoteTestRoot + "/" + options.remoteAppPath
+ elif (options.app == None):
+ # Neither remoteAppPath nor app are set -- error
+ print "ERROR: You must specify either appPath or app"
+ return None
+
+ if (options.xrePath == None):
+ print "ERROR: You must specify the path to the controller xre directory"
+ return None
+ else:
+ # Ensure xrepath is a full path
+ options.xrePath = os.path.abspath(options.xrePath)
+
+ # Default to <deviceroot>/reftest/reftest.log
+ if (options.remoteLogFile == None):
+ options.remoteLogFile = 'reftest.log'
+
+ options.localLogName = options.remoteLogFile
+ options.remoteLogFile = options.remoteTestRoot + '/' + options.remoteLogFile
+
+ # Ensure that the options.logfile (which the base class uses) is set to
+ # the remote setting when running remote. Also, if the user set the
+ # log file name there, use that instead of reusing the remotelogfile as above.
+ if (options.logFile):
+ # If the user specified a local logfile name use that
+ options.localLogName = options.logFile
+
+ options.logFile = options.remoteLogFile
+
+ if (options.pidFile != ""):
+ f = open(options.pidFile, 'w')
+ f.write("%s" % os.getpid())
+ f.close()
+
+ # httpd-path is specified by standard makefile targets and may be specified
+ # on the command line to select a particular version of httpd.js. If not
+ # specified, try to select the one from hostutils.zip, as required in bug 882932.
+ if not options.httpdPath:
+ options.httpdPath = os.path.join(options.utilityPath, "components")
+
+ # TODO: Copied from main, but I think these are no longer used in a post xulrunner world
+ #options.xrePath = options.remoteTestRoot + self._automation._product + '/xulrunner'
+ #options.utilityPath = options.testRoot + self._automation._product + '/bin'
+ return options
+
+class ReftestServer:
+ """ Web server used to serve Reftests, for closer fidelity to the real web.
+ It is virtually identical to the server used in mochitest and will only
+ be used for running reftests remotely.
+ Bug 581257 has been filed to refactor this wrapper around httpd.js into
+ it's own class and use it in both remote and non-remote testing. """
+
+ def __init__(self, automation, options, scriptDir):
+ self._automation = automation
+ self._utilityPath = options.utilityPath
+ self._xrePath = options.xrePath
+ self._profileDir = options.serverProfilePath
+ self.webServer = options.remoteWebServer
+ self.httpPort = options.httpPort
+ self.scriptDir = scriptDir
+ self.pidFile = options.pidFile
+ self._httpdPath = os.path.abspath(options.httpdPath)
+ self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort }
+
+ def start(self):
+ "Run the Refest server, returning the process ID of the server."
+
+ env = self._automation.environment(xrePath = self._xrePath)
+ env["XPCOM_DEBUG_BREAK"] = "warn"
+ if self._automation.IS_WIN32:
+ env["PATH"] = env["PATH"] + ";" + self._xrePath
+
+ args = ["-g", self._xrePath,
+ "-v", "170",
+ "-f", os.path.join(self._httpdPath, "httpd.js"),
+ "-e", "const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR ='%(server)s';" %
+ {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer },
+ "-f", os.path.join(self.scriptDir, "server.js")]
+
+ xpcshell = os.path.join(self._utilityPath,
+ "xpcshell" + self._automation.BIN_SUFFIX)
+
+ if not os.access(xpcshell, os.F_OK):
+ raise Exception('xpcshell not found at %s' % xpcshell)
+ if self._automation.elf_arm(xpcshell):
+ raise Exception('xpcshell at %s is an ARM binary; please use '
+ 'the --utility-path argument to specify the path '
+ 'to a desktop version.' % xpcshell)
+
+ self._process = self._automation.Process([xpcshell] + args, env = env)
+ pid = self._process.pid
+ if pid < 0:
+ print "TEST-UNEXPECTED-FAIL | remotereftests.py | Error starting server."
+ return 2
+ self._automation.log.info("INFO | remotereftests.py | Server pid: %d", pid)
+
+ if (self.pidFile != ""):
+ f = open(self.pidFile + ".xpcshell.pid", 'w')
+ f.write("%s" % pid)
+ f.close()
+
+ def ensureReady(self, timeout):
+ assert timeout >= 0
+
+ aliveFile = os.path.join(self._profileDir, "server_alive.txt")
+ i = 0
+ while i < timeout:
+ if os.path.exists(aliveFile):
+ break
+ time.sleep(1)
+ i += 1
+ else:
+ print "TEST-UNEXPECTED-FAIL | remotereftests.py | Timed out while waiting for server startup."
+ self.stop()
+ return 1
+
+ def stop(self):
+ if hasattr(self, '_process'):
+ try:
+ c = urllib2.urlopen(self.shutdownURL)
+ c.read()
+ c.close()
+
+ rtncode = self._process.poll()
+ if (rtncode == None):
+ self._process.terminate()
+ except:
+ self._process.kill()
+
+class RemoteReftest(RefTest):
+ remoteApp = ''
+
+ def __init__(self, automation, devicemanager, options, scriptDir):
+ RefTest.__init__(self, automation)
+ self._devicemanager = devicemanager
+ self.scriptDir = scriptDir
+ self.remoteApp = options.app
+ self.remoteProfile = options.remoteProfile
+ self.remoteTestRoot = options.remoteTestRoot
+ self.remoteLogFile = options.remoteLogFile
+ self.localLogName = options.localLogName
+ self.pidFile = options.pidFile
+ if self.automation.IS_DEBUG_BUILD:
+ self.SERVER_STARTUP_TIMEOUT = 180
+ else:
+ self.SERVER_STARTUP_TIMEOUT = 90
+ self.automation.deleteANRs()
+
+ def findPath(self, paths, filename = None):
+ for path in paths:
+ p = path
+ if filename:
+ p = os.path.join(p, filename)
+ if os.path.exists(self.getFullPath(p)):
+ return path
+ return None
+
+ def startWebServer(self, options):
+ """ Create the webserver on the host and start it up """
+ remoteXrePath = options.xrePath
+ remoteUtilityPath = options.utilityPath
+ localAutomation = Automation()
+ localAutomation.IS_WIN32 = False
+ localAutomation.IS_LINUX = False
+ localAutomation.IS_MAC = False
+ localAutomation.UNIXISH = False
+ hostos = sys.platform
+ if (hostos == 'mac' or hostos == 'darwin'):
+ localAutomation.IS_MAC = True
+ elif (hostos == 'linux' or hostos == 'linux2'):
+ localAutomation.IS_LINUX = True
+ localAutomation.UNIXISH = True
+ elif (hostos == 'win32' or hostos == 'win64'):
+ localAutomation.BIN_SUFFIX = ".exe"
+ localAutomation.IS_WIN32 = True
+
+ paths = [options.xrePath, localAutomation.DIST_BIN, self.automation._product, os.path.join('..', self.automation._product)]
+ options.xrePath = self.findPath(paths)
+ if options.xrePath == None:
+ print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name)
+ return 1
+ paths.append("bin")
+ paths.append(os.path.join("..", "bin"))
+
+ xpcshell = "xpcshell"
+ if (os.name == "nt"):
+ xpcshell += ".exe"
+
+ if (options.utilityPath):
+ paths.insert(0, options.utilityPath)
+ options.utilityPath = self.findPath(paths, xpcshell)
+ if options.utilityPath == None:
+ print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name)
+ return 1
+
+ options.serverProfilePath = tempfile.mkdtemp()
+ self.server = ReftestServer(localAutomation, options, self.scriptDir)
+ retVal = self.server.start()
+ if retVal:
+ return retVal
+ retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+ if retVal:
+ return retVal
+
+ options.xrePath = remoteXrePath
+ options.utilityPath = remoteUtilityPath
+ return 0
+
+ def stopWebServer(self, options):
+ self.server.stop()
+
+ def createReftestProfile(self, options, profileDir, reftestlist):
+ RefTest.createReftestProfile(self, options, profileDir, reftestlist, server=options.remoteWebServer)
+
+ # Turn off the locale picker screen
+ fhandle = open(os.path.join(profileDir, "user.js"), 'a')
+ fhandle.write("""
+user_pref("browser.firstrun.show.localepicker", false);
+user_pref("font.size.inflation.emPerLine", 0);
+user_pref("font.size.inflation.minTwips", 0);
+user_pref("reftest.remote", true);
+// Set a future policy version to avoid the telemetry prompt.
+user_pref("toolkit.telemetry.prompted", 999);
+user_pref("toolkit.telemetry.notifiedOptOut", 999);
+user_pref("reftest.uri", "%s");
+user_pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", true);
+
+// Point the url-classifier to the local testing server for fast failures
+user_pref("browser.safebrowsing.gethashURL", "http://127.0.0.1:8888/safebrowsing-dummy/gethash");
+user_pref("browser.safebrowsing.keyURL", "http://127.0.0.1:8888/safebrowsing-dummy/newkey");
+user_pref("browser.safebrowsing.updateURL", "http://127.0.0.1:8888/safebrowsing-dummy/update");
+// Point update checks to the local testing server for fast failures
+user_pref("extensions.update.url", "http://127.0.0.1:8888/extensions-dummy/updateURL");
+user_pref("extensions.update.background.url", "http://127.0.0.1:8888/extensions-dummy/updateBackgroundURL");
+user_pref("extensions.blocklist.url", "http://127.0.0.1:8888/extensions-dummy/blocklistURL");
+user_pref("extensions.hotfix.url", "http://127.0.0.1:8888/extensions-dummy/hotfixURL");
+// Turn off extension updates so they don't bother tests
+user_pref("extensions.update.enabled", false);
+// Make sure opening about:addons won't hit the network
+user_pref("extensions.webservice.discoverURL", "http://127.0.0.1:8888/extensions-dummy/discoveryURL");
+// Make sure AddonRepository won't hit the network
+user_pref("extensions.getAddons.maxResults", 0);
+user_pref("extensions.getAddons.get.url", "http://127.0.0.1:8888/extensions-dummy/repositoryGetURL");
+user_pref("extensions.getAddons.getWithPerformance.url", "http://127.0.0.1:8888/extensions-dummy/repositoryGetWithPerformanceURL");
+user_pref("extensions.getAddons.search.browseURL", "http://127.0.0.1:8888/extensions-dummy/repositoryBrowseURL");
+user_pref("extensions.getAddons.search.url", "http://127.0.0.1:8888/extensions-dummy/repositorySearchURL");
+// Make sure that opening the plugins check page won't hit the network
+user_pref("plugins.update.url", "http://127.0.0.1:8888/plugins-dummy/updateCheckURL");
+
+""" % reftestlist)
+
+ #workaround for jsreftests.
+ if options.enablePrivilege:
+ fhandle.write("""
+user_pref("capability.principal.codebase.p2.granted", "UniversalXPConnect");
+user_pref("capability.principal.codebase.p2.id", "http://%s:%s");
+""" % (options.remoteWebServer, options.httpPort))
+
+ # Close the file
+ fhandle.close()
+
+ try:
+ self._devicemanager.pushDir(profileDir, options.remoteProfile)
+ except devicemanager.DMError:
+ print "Automation Error: Failed to copy profiledir to device"
+ raise
+
+ def copyExtraFilesToProfile(self, options, profileDir):
+ RefTest.copyExtraFilesToProfile(self, options, profileDir)
+ try:
+ self._devicemanager.pushDir(profileDir, options.remoteProfile)
+ except devicemanager.DMError:
+ print "Automation Error: Failed to copy extra files to device"
+ raise
+
+ def getManifestPath(self, path):
+ return path
+
+ def cleanup(self, profileDir):
+ # Pull results back from device
+ if self.remoteLogFile and \
+ self._devicemanager.fileExists(self.remoteLogFile):
+ self._devicemanager.getFile(self.remoteLogFile, self.localLogName)
+ else:
+ print "WARNING: Unable to retrieve log file (%s) from remote " \
+ "device" % self.remoteLogFile
+ self._devicemanager.removeDir(self.remoteProfile)
+ self._devicemanager.removeDir(self.remoteTestRoot)
+ RefTest.cleanup(self, profileDir)
+ if (self.pidFile != ""):
+ try:
+ os.remove(self.pidFile)
+ os.remove(self.pidFile + ".xpcshell.pid")
+ except:
+ print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % self.pidFile
+
+def main(args):
+ automation = RemoteAutomation(None)
+ parser = RemoteOptions(automation)
+ options, args = parser.parse_args()
+
+ if (options.deviceIP == None):
+ print "Error: you must provide a device IP to connect to via the --device option"
+ return 1
+
+ try:
+ if (options.dm_trans == "adb"):
+ if (options.deviceIP):
+ dm = droid.DroidADB(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
+ else:
+ dm = droid.DroidADB(None, None, deviceRoot=options.remoteTestRoot)
+ else:
+ dm = droid.DroidSUT(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
+ except devicemanager.DMError:
+ print "Automation Error: exception while initializing devicemanager. Most likely the device is not in a testable state."
+ return 1
+
+ automation.setDeviceManager(dm)
+
+ if (options.remoteProductName != None):
+ automation.setProduct(options.remoteProductName)
+
+ # Set up the defaults and ensure options are set
+ options = parser.verifyRemoteOptions(options)
+ if (options == None):
+ print "ERROR: Invalid options specified, use --help for a list of valid options"
+ return 1
+
+ if not options.ignoreWindowSize:
+ parts = dm.getInfo('screen')['screen'][0].split()
+ width = int(parts[0].split(':')[1])
+ height = int(parts[1].split(':')[1])
+ if (width < 1050 or height < 1050):
+ print "ERROR: Invalid screen resolution %sx%s, please adjust to 1366x1050 or higher" % (width, height)
+ return 1
+
+ automation.setAppName(options.app)
+ automation.setRemoteProfile(options.remoteProfile)
+ automation.setRemoteLog(options.remoteLogFile)
+ reftest = RemoteReftest(automation, dm, options, SCRIPT_DIRECTORY)
+ options = parser.verifyCommonOptions(options, reftest)
+
+ # Hack in a symbolic link for jsreftest
+ os.system("ln -s ../jsreftest " + str(os.path.join(SCRIPT_DIRECTORY, "jsreftest")))
+
+ # Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside' the webroot
+ manifest = args[0]
+ if os.path.exists(os.path.join(SCRIPT_DIRECTORY, args[0])):
+ manifest = "http://" + str(options.remoteWebServer) + ":" + str(options.httpPort) + "/" + args[0]
+ elif os.path.exists(args[0]):
+ manifestPath = os.path.abspath(args[0]).split(SCRIPT_DIRECTORY)[1].strip('/')
+ manifest = "http://" + str(options.remoteWebServer) + ":" + str(options.httpPort) + "/" + manifestPath
+ else:
+ print "ERROR: Could not find test manifest '%s'" % manifest
+ return 1
+
+ # Start the webserver
+ retVal = reftest.startWebServer(options)
+ if retVal:
+ return retVal
+
+ procName = options.app.split('/')[-1]
+ if (dm.processExist(procName)):
+ dm.killProcess(procName)
+
+ print dm.getInfo()
+
+#an example manifest name to use on the cli
+# manifest = "http://" + options.remoteWebServer + "/reftests/layout/reftests/reftest-sanity/reftest.list"
+ retVal = 0
+ try:
+ cmdlineArgs = ["-reftest", manifest]
+ if options.bootstrap:
+ cmdlineArgs = []
+ dm.recordLogcat()
+ retVal = reftest.runTests(manifest, options, cmdlineArgs)
+ except:
+ print "Automation Error: Exception caught while running tests"
+ traceback.print_exc()
+ retVal = 1
+
+ reftest.stopWebServer(options)
+ try:
+ logcat = dm.getLogcat(filterOutRegexps=fennecLogcatFilters)
+ print ''.join(logcat)
+ print dm.getInfo()
+ except devicemanager.DMError:
+ print "WARNING: Error getting device information at end of test"
+
+ return retVal
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
+
diff --git a/layout/tools/reftest/runreftest.py b/layout/tools/reftest/runreftest.py
new file mode 100644
index 000000000..683b0a1a1
--- /dev/null
+++ b/layout/tools/reftest/runreftest.py
@@ -0,0 +1,322 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Runs the reftest test harness.
+"""
+
+import re, sys, shutil, os, os.path
+SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
+sys.path.insert(0, SCRIPT_DIRECTORY)
+
+from automation import Automation
+from automationutils import *
+from optparse import OptionParser
+from tempfile import mkdtemp
+
+class RefTest(object):
+
+ oldcwd = os.getcwd()
+
+ def __init__(self, automation):
+ self.automation = automation
+
+ def getFullPath(self, path):
+ "Get an absolute path relative to self.oldcwd."
+ return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
+
+ def getManifestPath(self, path):
+ "Get the path of the manifest, and for remote testing this function is subclassed to point to remote manifest"
+ path = self.getFullPath(path)
+ if os.path.isdir(path):
+ defaultManifestPath = os.path.join(path, 'reftest.list')
+ if os.path.exists(defaultManifestPath):
+ path = defaultManifestPath
+ else:
+ defaultManifestPath = os.path.join(path, 'crashtests.list')
+ if os.path.exists(defaultManifestPath):
+ path = defaultManifestPath
+ return path
+
+ def makeJSString(self, s):
+ return '"%s"' % re.sub(r'([\\"])', r'\\\1', s)
+
+ def createReftestProfile(self, options, profileDir, manifest, server='localhost'):
+ """
+ Sets up a profile for reftest.
+ 'manifest' is the path to the reftest.list file we want to test with. This is used in
+ the remote subclass in remotereftest.py so we can write it to a preference for the
+ bootstrap extension.
+ """
+
+ self.automation.setupPermissionsDatabase(profileDir,
+ {'allowXULXBL': [(server, True), ('<file>', True)]})
+
+ # Set preferences for communication between our command line arguments
+ # and the reftest harness. Preferences that are required for reftest
+ # to work should instead be set in reftest-cmdline.js .
+ prefsFile = open(os.path.join(profileDir, "user.js"), "a")
+ prefsFile.write('user_pref("reftest.timeout", %d);\n' % (options.timeout * 1000))
+
+ if options.totalChunks != None:
+ prefsFile.write('user_pref("reftest.totalChunks", %d);\n' % options.totalChunks)
+ if options.thisChunk != None:
+ prefsFile.write('user_pref("reftest.thisChunk", %d);\n' % options.thisChunk)
+ if options.logFile != None:
+ prefsFile.write('user_pref("reftest.logFile", "%s");\n' % options.logFile)
+ if options.ignoreWindowSize != False:
+ prefsFile.write('user_pref("reftest.ignoreWindowSize", true);\n')
+ if options.filter != None:
+ prefsFile.write('user_pref("reftest.filter", %s);\n' % self.makeJSString(options.filter))
+ prefsFile.write('user_pref("reftest.focusFilterMode", %s);\n' % self.makeJSString(options.focusFilterMode))
+
+ # Ensure that telemetry is disabled, so we don't connect to the telemetry
+ # server in the middle of the tests.
+ prefsFile.write('user_pref("toolkit.telemetry.enabled", false);\n')
+
+ for v in options.extraPrefs:
+ thispref = v.split("=")
+ if len(thispref) < 2:
+ print "Error: syntax error in --setpref=" + v
+ sys.exit(1)
+ part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
+ prefsFile.write(part)
+ prefsFile.close()
+
+ # install the reftest extension bits into the profile
+ self.automation.installExtension(os.path.join(SCRIPT_DIRECTORY, "reftest"),
+ profileDir,
+ "reftest@mozilla.org")
+
+ # I would prefer to use "--install-extension reftest/specialpowers", but that requires tight coordination with
+ # release engineering and landing on multiple branches at once.
+ if manifest.endswith('crashtests.list'):
+ self.automation.installExtension(os.path.join(SCRIPT_DIRECTORY, "specialpowers"),
+ profileDir,
+ "special-powers@mozilla.org")
+
+ def buildBrowserEnv(self, options, profileDir):
+ browserEnv = self.automation.environment(xrePath = options.xrePath)
+ browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
+
+ for v in options.environment:
+ ix = v.find("=")
+ if ix <= 0:
+ print "Error: syntax error in --setenv=" + v
+ return None
+ browserEnv[v[:ix]] = v[ix + 1:]
+
+ # Enable leaks detection to its own log file.
+ self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
+ browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile
+ return browserEnv
+
+ def cleanup(self, profileDir):
+ if profileDir:
+ shutil.rmtree(profileDir, True)
+
+ def runTests(self, testPath, options, cmdlineArgs = None):
+ debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs,
+ options.debuggerInteractive);
+
+ profileDir = None
+ try:
+ reftestlist = self.getManifestPath(testPath)
+ if cmdlineArgs == None:
+ cmdlineArgs = ['-reftest', reftestlist]
+ profileDir = mkdtemp()
+ self.copyExtraFilesToProfile(options, profileDir)
+ self.createReftestProfile(options, profileDir, reftestlist)
+ self.installExtensionsToProfile(options, profileDir)
+
+ # browser environment
+ browserEnv = self.buildBrowserEnv(options, profileDir)
+
+ self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n")
+ status = self.automation.runApp(None, browserEnv, options.app, profileDir,
+ cmdlineArgs,
+ utilityPath = options.utilityPath,
+ xrePath=options.xrePath,
+ debuggerInfo=debuggerInfo,
+ symbolsPath=options.symbolsPath,
+ # give the JS harness 30 seconds to deal
+ # with its own timeouts
+ timeout=options.timeout + 30.0)
+ processLeakLog(self.leakLogFile, options.leakThreshold)
+ self.automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.")
+ finally:
+ self.cleanup(profileDir)
+ return status
+
+ def copyExtraFilesToProfile(self, options, profileDir):
+ "Copy extra files or dirs specified on the command line to the testing profile."
+ for f in options.extraProfileFiles:
+ abspath = self.getFullPath(f)
+ if os.path.isfile(abspath):
+ shutil.copy2(abspath, profileDir)
+ elif os.path.isdir(abspath):
+ dest = os.path.join(profileDir, os.path.basename(abspath))
+ shutil.copytree(abspath, dest)
+ else:
+ self.automation.log.warning("WARNING | runreftest.py | Failed to copy %s to profile", abspath)
+ continue
+
+ def installExtensionsToProfile(self, options, profileDir):
+ "Install application distributed extensions and specified on the command line ones to testing profile."
+ # Install distributed extensions, if application has any.
+ distExtDir = os.path.join(options.app[ : options.app.rfind(os.sep)], "distribution", "extensions")
+ if os.path.isdir(distExtDir):
+ for f in os.listdir(distExtDir):
+ self.automation.installExtension(os.path.join(distExtDir, f), profileDir)
+
+ # Install custom extensions.
+ for f in options.extensionsToInstall:
+ self.automation.installExtension(self.getFullPath(f), profileDir)
+
+
+class ReftestOptions(OptionParser):
+
+ def __init__(self, automation):
+ self._automation = automation
+ OptionParser.__init__(self)
+ defaults = {}
+
+ # we want to pass down everything from automation.__all__
+ addCommonOptions(self,
+ defaults=dict(zip(self._automation.__all__,
+ [getattr(self._automation, x) for x in self._automation.__all__])))
+ self._automation.addCommonOptions(self)
+ self.add_option("--appname",
+ action = "store", type = "string", dest = "app",
+ default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP),
+ help = "absolute path to application, overriding default")
+ self.add_option("--extra-profile-file",
+ action = "append", dest = "extraProfileFiles",
+ default = [],
+ help = "copy specified files/dirs to testing profile")
+ self.add_option("--timeout",
+ action = "store", dest = "timeout", type = "int",
+ default = 5 * 60, # 5 minutes per bug 479518
+ help = "reftest will timeout in specified number of seconds. [default %default s].")
+ self.add_option("--leak-threshold",
+ action = "store", type = "int", dest = "leakThreshold",
+ default = 0,
+ help = "fail if the number of bytes leaked through "
+ "refcounted objects (or bytes in classes with "
+ "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater "
+ "than the given number")
+ self.add_option("--utility-path",
+ action = "store", type = "string", dest = "utilityPath",
+ default = self._automation.DIST_BIN,
+ help = "absolute path to directory containing utility "
+ "programs (xpcshell, ssltunnel, certutil)")
+ defaults["utilityPath"] = self._automation.DIST_BIN
+
+ self.add_option("--total-chunks",
+ type = "int", dest = "totalChunks",
+ help = "how many chunks to split the tests up into")
+ defaults["totalChunks"] = None
+
+ self.add_option("--this-chunk",
+ type = "int", dest = "thisChunk",
+ help = "which chunk to run between 1 and --total-chunks")
+ defaults["thisChunk"] = None
+
+ self.add_option("--log-file",
+ action = "store", type = "string", dest = "logFile",
+ default = None,
+ help = "file to log output to in addition to stdout")
+ defaults["logFile"] = None
+
+ self.add_option("--skip-slow-tests",
+ dest = "skipSlowTests", action = "store_true",
+ help = "skip tests marked as slow when running")
+ defaults["skipSlowTests"] = False
+
+ self.add_option("--ignore-window-size",
+ dest = "ignoreWindowSize", action = "store_true",
+ help = "ignore the window size, which may cause spurious failures and passes")
+ defaults["ignoreWindowSize"] = False
+
+ self.add_option("--install-extension",
+ action = "append", dest = "extensionsToInstall",
+ help = "install the specified extension in the testing profile. "
+ "The extension file's name should be <id>.xpi where <id> is "
+ "the extension's id as indicated in its install.rdf. "
+ "An optional path can be specified too.")
+ defaults["extensionsToInstall"] = []
+
+ self.add_option("--setenv",
+ action = "append", type = "string",
+ dest = "environment", metavar = "NAME=VALUE",
+ help = "sets the given variable in the application's "
+ "environment")
+ defaults["environment"] = []
+
+ self.add_option("--filter",
+ action = "store", type="string", dest = "filter",
+ help = "specifies a regular expression (as could be passed to the JS "
+ "RegExp constructor) to test against URLs in the reftest manifest; "
+ "only test items that have a matching test URL will be run.")
+ defaults["filter"] = None
+
+ self.add_option("--focus-filter-mode",
+ action = "store", type = "string", dest = "focusFilterMode",
+ help = "filters tests to run by whether they require focus. "
+ "Valid values are `all', `needs-focus', or `non-needs-focus'. "
+ "Defaults to `all'.")
+ defaults["focusFilterMode"] = "all"
+
+ self.set_defaults(**defaults)
+
+ def verifyCommonOptions(self, options, reftest):
+ if options.totalChunks is not None and options.thisChunk is None:
+ self.error("thisChunk must be specified when totalChunks is specified")
+
+ if options.totalChunks:
+ if not 1 <= options.thisChunk <= options.totalChunks:
+ self.error("thisChunk must be between 1 and totalChunks")
+
+ if options.logFile:
+ options.logFile = reftest.getFullPath(options.logFile)
+
+ if options.xrePath is not None:
+ if not os.access(options.xrePath, os.F_OK):
+ self.error("--xre-path '%s' not found" % options.xrePath)
+ if not os.path.isdir(options.xrePath):
+ self.error("--xre-path '%s' is not a directory" % options.xrePath)
+ options.xrePath = reftest.getFullPath(options.xrePath)
+
+ return options
+
+def main():
+ automation = Automation()
+ parser = ReftestOptions(automation)
+ reftest = RefTest(automation)
+
+ options, args = parser.parse_args()
+ if len(args) != 1:
+ print >>sys.stderr, "No reftest.list specified."
+ sys.exit(1)
+
+ options = parser.verifyCommonOptions(options, reftest)
+
+ options.app = reftest.getFullPath(options.app)
+ if not os.path.exists(options.app):
+ print """Error: Path %(app)s doesn't exist.
+Are you executing $objdir/_tests/reftest/runreftest.py?""" \
+ % {"app": options.app}
+ sys.exit(1)
+
+ if options.xrePath is None:
+ options.xrePath = os.path.dirname(options.app)
+
+ if options.symbolsPath and not isURL(options.symbolsPath):
+ options.symbolsPath = reftest.getFullPath(options.symbolsPath)
+ options.utilityPath = reftest.getFullPath(options.utilityPath)
+
+ sys.exit(reftest.runTests(args[0], options))
+
+if __name__ == "__main__":
+ main()
diff --git a/layout/tools/reftest/runreftestb2g.py b/layout/tools/reftest/runreftestb2g.py
new file mode 100644
index 000000000..ffd7b5e01
--- /dev/null
+++ b/layout/tools/reftest/runreftestb2g.py
@@ -0,0 +1,584 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import ConfigParser
+import os
+import sys
+import tempfile
+import traceback
+
+# We need to know our current directory so that we can serve our test files from it.
+SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
+sys.path.insert(0, SCRIPT_DIRECTORY)
+
+from automation import Automation
+from b2gautomation import B2GRemoteAutomation
+from runreftest import RefTest
+from runreftest import ReftestOptions
+from remotereftest import ReftestServer
+
+from mozdevice import DeviceManagerADB, DMError
+from marionette import Marionette
+
+
+class B2GOptions(ReftestOptions):
+
+ def __init__(self, automation, **kwargs):
+ defaults = {}
+ ReftestOptions.__init__(self, automation)
+
+ self.add_option("--b2gpath", action="store",
+ type = "string", dest = "b2gPath",
+ help = "path to B2G repo or qemu dir")
+ defaults["b2gPath"] = None
+
+ self.add_option("--marionette", action="store",
+ type = "string", dest = "marionette",
+ help = "host:port to use when connecting to Marionette")
+ defaults["marionette"] = None
+
+ self.add_option("--emulator", action="store",
+ type="string", dest = "emulator",
+ help = "Architecture of emulator to use: x86 or arm")
+ defaults["emulator"] = None
+ self.add_option("--emulator-res", action="store",
+ type="string", dest = "emulator_res",
+ help = "Emulator resolution of the format '<width>x<height>'")
+ defaults["emulator_res"] = None
+
+ self.add_option("--no-window", action="store_true",
+ dest = "noWindow",
+ help = "Pass --no-window to the emulator")
+ defaults["noWindow"] = False
+
+ self.add_option("--adbpath", action="store",
+ type = "string", dest = "adbPath",
+ help = "path to adb")
+ defaults["adbPath"] = "adb"
+
+ self.add_option("--deviceIP", action="store",
+ type = "string", dest = "deviceIP",
+ help = "ip address of remote device to test")
+ defaults["deviceIP"] = None
+
+ self.add_option("--devicePort", action="store",
+ type = "string", dest = "devicePort",
+ help = "port of remote device to test")
+ defaults["devicePort"] = 20701
+
+ self.add_option("--remote-logfile", action="store",
+ type = "string", dest = "remoteLogFile",
+ help = "Name of log file on the device relative to the device root. PLEASE ONLY USE A FILENAME.")
+ defaults["remoteLogFile"] = None
+
+ self.add_option("--remote-webserver", action = "store",
+ type = "string", dest = "remoteWebServer",
+ help = "ip address where the remote web server is hosted at")
+ defaults["remoteWebServer"] = None
+
+ self.add_option("--http-port", action = "store",
+ type = "string", dest = "httpPort",
+ help = "ip address where the remote web server is hosted at")
+ defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
+
+ self.add_option("--ssl-port", action = "store",
+ type = "string", dest = "sslPort",
+ help = "ip address where the remote web server is hosted at")
+ defaults["sslPort"] = automation.DEFAULT_SSL_PORT
+
+ self.add_option("--pidfile", action = "store",
+ type = "string", dest = "pidFile",
+ help = "name of the pidfile to generate")
+ defaults["pidFile"] = ""
+ self.add_option("--gecko-path", action="store",
+ type="string", dest="geckoPath",
+ help="the path to a gecko distribution that should "
+ "be installed on the emulator prior to test")
+ defaults["geckoPath"] = None
+ self.add_option("--logcat-dir", action="store",
+ type="string", dest="logcat_dir",
+ help="directory to store logcat dump files")
+ defaults["logcat_dir"] = None
+ self.add_option('--busybox', action='store',
+ type='string', dest='busybox',
+ help="Path to busybox binary to install on device")
+ defaults['busybox'] = None
+ self.add_option("--httpd-path", action = "store",
+ type = "string", dest = "httpdPath",
+ help = "path to the httpd.js file")
+ defaults["httpdPath"] = None
+ defaults["remoteTestRoot"] = "/data/local/tests"
+ defaults["logFile"] = "reftest.log"
+ defaults["autorun"] = True
+ defaults["closeWhenDone"] = True
+ defaults["testPath"] = ""
+
+ self.set_defaults(**defaults)
+
+ def verifyRemoteOptions(self, options):
+ if not options.remoteTestRoot:
+ options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + "/reftest"
+ options.remoteProfile = options.remoteTestRoot + "/profile"
+
+ productRoot = options.remoteTestRoot + "/" + self._automation._product
+ if options.utilityPath == self._automation.DIST_BIN:
+ options.utilityPath = productRoot + "/bin"
+
+ if options.remoteWebServer == None:
+ if os.name != "nt":
+ options.remoteWebServer = self._automation.getLanIp()
+ else:
+ print "ERROR: you must specify a --remote-webserver=<ip address>\n"
+ return None
+
+ options.webServer = options.remoteWebServer
+
+ if options.geckoPath and not options.emulator:
+ self.error("You must specify --emulator if you specify --gecko-path")
+
+ if options.logcat_dir and not options.emulator:
+ self.error("You must specify --emulator if you specify --logcat-dir")
+
+ #if not options.emulator and not options.deviceIP:
+ # print "ERROR: you must provide a device IP"
+ # return None
+
+ if options.remoteLogFile == None:
+ options.remoteLogFile = "reftest.log"
+
+ options.localLogName = options.remoteLogFile
+ options.remoteLogFile = options.remoteTestRoot + '/' + options.remoteLogFile
+
+ # Ensure that the options.logfile (which the base class uses) is set to
+ # the remote setting when running remote. Also, if the user set the
+ # log file name there, use that instead of reusing the remotelogfile as above.
+ if (options.logFile):
+ # If the user specified a local logfile name use that
+ options.localLogName = options.logFile
+ options.logFile = options.remoteLogFile
+
+ # Only reset the xrePath if it wasn't provided
+ if options.xrePath == None:
+ options.xrePath = options.utilityPath
+ options.xrePath = os.path.abspath(options.xrePath)
+
+ if options.pidFile != "":
+ f = open(options.pidFile, 'w')
+ f.write("%s" % os.getpid())
+ f.close()
+
+ # httpd-path is specified by standard makefile targets and may be specified
+ # on the command line to select a particular version of httpd.js. If not
+ # specified, try to select the one from from the xre bundle, as required in bug 882932.
+ if not options.httpdPath:
+ options.httpdPath = os.path.join(options.xrePath, "components")
+
+ return options
+
+
+class ProfileConfigParser(ConfigParser.RawConfigParser):
+ """Subclass of RawConfigParser that outputs .ini files in the exact
+ format expected for profiles.ini, which is slightly different
+ than the default format.
+ """
+
+ def optionxform(self, optionstr):
+ return optionstr
+
+ def write(self, fp):
+ if self._defaults:
+ fp.write("[%s]\n" % ConfigParser.DEFAULTSECT)
+ for (key, value) in self._defaults.items():
+ fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t')))
+ fp.write("\n")
+ for section in self._sections:
+ fp.write("[%s]\n" % section)
+ for (key, value) in self._sections[section].items():
+ if key == "__name__":
+ continue
+ if (value is not None) or (self._optcre == self.OPTCRE):
+ key = "=".join((key, str(value).replace('\n', '\n\t')))
+ fp.write("%s\n" % (key))
+ fp.write("\n")
+
+
+class B2GReftest(RefTest):
+
+ _automation = None
+ _devicemanager = None
+ localProfile = None
+ remoteApp = ''
+ profile = None
+
+ def __init__(self, automation, devicemanager, options, scriptDir):
+ self._automation = automation
+ RefTest.__init__(self, self._automation)
+ self._devicemanager = devicemanager
+ self.runSSLTunnel = False
+ self.remoteTestRoot = options.remoteTestRoot
+ self.remoteProfile = options.remoteProfile
+ self._automation.setRemoteProfile(self.remoteProfile)
+ self.localLogName = options.localLogName
+ self.remoteLogFile = options.remoteLogFile
+ self.bundlesDir = '/system/b2g/distribution/bundles'
+ self.userJS = '/data/local/user.js'
+ self.remoteMozillaPath = '/data/b2g/mozilla'
+ self.remoteProfilesIniPath = os.path.join(self.remoteMozillaPath, 'profiles.ini')
+ self.originalProfilesIni = None
+ self.scriptDir = scriptDir
+ self.SERVER_STARTUP_TIMEOUT = 90
+ if self._automation.IS_DEBUG_BUILD:
+ self.SERVER_STARTUP_TIMEOUT = 180
+
+ def cleanup(self, profileDir):
+ # Pull results back from device
+ if (self.remoteLogFile):
+ try:
+ self._devicemanager.getFile(self.remoteLogFile, self.localLogName)
+ except:
+ print "ERROR: We were not able to retrieve the info from %s" % self.remoteLogFile
+ sys.exit(5)
+
+ # Delete any bundled extensions
+ extensionDir = os.path.join(profileDir, 'extensions', 'staged')
+ for filename in os.listdir(extensionDir):
+ try:
+ self._devicemanager._checkCmdAs(['shell', 'rm', '-rf',
+ os.path.join(self.bundlesDir, filename)])
+ except DMError:
+ pass
+
+ # Restore the original profiles.ini.
+ if self.originalProfilesIni:
+ try:
+ if not self._automation._is_emulator:
+ self.restoreProfilesIni()
+ os.remove(self.originalProfilesIni)
+ except:
+ pass
+
+ if not self._automation._is_emulator:
+ self._devicemanager.removeFile(self.remoteLogFile)
+ self._devicemanager.removeDir(self.remoteProfile)
+ self._devicemanager.removeDir(self.remoteTestRoot)
+
+ # Restore the original user.js.
+ self._devicemanager._checkCmdAs(['shell', 'rm', '-f', self.userJS])
+ self._devicemanager._checkCmdAs(['shell', 'dd', 'if=%s.orig' % self.userJS, 'of=%s' % self.userJS])
+
+ # We've restored the original profile, so reboot the device so that
+ # it gets picked up.
+ self._automation.rebootDevice()
+
+ RefTest.cleanup(self, profileDir)
+ if getattr(self, 'pidFile', '') != '':
+ try:
+ os.remove(self.pidFile)
+ os.remove(self.pidFile + ".xpcshell.pid")
+ except:
+ print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % self.pidFile
+
+ def findPath(self, paths, filename = None):
+ for path in paths:
+ p = path
+ if filename:
+ p = os.path.join(p, filename)
+ if os.path.exists(self.getFullPath(p)):
+ return path
+ return None
+
+ def startWebServer(self, options):
+ """ Create the webserver on the host and start it up """
+ remoteXrePath = options.xrePath
+ remoteProfilePath = self.remoteProfile
+ remoteUtilityPath = options.utilityPath
+ localAutomation = Automation()
+ localAutomation.IS_WIN32 = False
+ localAutomation.IS_LINUX = False
+ localAutomation.IS_MAC = False
+ localAutomation.UNIXISH = False
+ hostos = sys.platform
+ if hostos in ['mac', 'darwin']:
+ localAutomation.IS_MAC = True
+ elif hostos in ['linux', 'linux2']:
+ localAutomation.IS_LINUX = True
+ localAutomation.UNIXISH = True
+ elif hostos in ['win32', 'win64']:
+ localAutomation.BIN_SUFFIX = ".exe"
+ localAutomation.IS_WIN32 = True
+
+ paths = [options.xrePath,
+ localAutomation.DIST_BIN,
+ self._automation._product,
+ os.path.join('..', self._automation._product)]
+ options.xrePath = self.findPath(paths)
+ if options.xrePath == None:
+ print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name)
+ sys.exit(1)
+ paths.append("bin")
+ paths.append(os.path.join("..", "bin"))
+
+ xpcshell = "xpcshell"
+ if (os.name == "nt"):
+ xpcshell += ".exe"
+
+ if (options.utilityPath):
+ paths.insert(0, options.utilityPath)
+ options.utilityPath = self.findPath(paths, xpcshell)
+ if options.utilityPath == None:
+ print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name)
+ sys.exit(1)
+
+ xpcshell = os.path.join(options.utilityPath, xpcshell)
+ if self._automation.elf_arm(xpcshell):
+ raise Exception('xpcshell at %s is an ARM binary; please use '
+ 'the --utility-path argument to specify the path '
+ 'to a desktop version.' % xpcshell)
+
+ options.serverProfilePath = tempfile.mkdtemp()
+ self.server = ReftestServer(localAutomation, options, self.scriptDir)
+ retVal = self.server.start()
+ if retVal:
+ return retVal
+
+ if (options.pidFile != ""):
+ f = open(options.pidFile + ".xpcshell.pid", 'w')
+ f.write("%s" % self.server._process.pid)
+ f.close()
+
+ retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+ if retVal:
+ return retVal
+
+ options.xrePath = remoteXrePath
+ options.utilityPath = remoteUtilityPath
+ options.profilePath = remoteProfilePath
+ return 0
+
+ def stopWebServer(self, options):
+ if hasattr(self, 'server'):
+ self.server.stop()
+
+
+ def restoreProfilesIni(self):
+ # restore profiles.ini on the device to its previous state
+ if not self.originalProfilesIni or not os.access(self.originalProfilesIni, os.F_OK):
+ raise DMError('Unable to install original profiles.ini; file not found: %s',
+ self.originalProfilesIni)
+
+ self._devicemanager.pushFile(self.originalProfilesIni, self.remoteProfilesIniPath)
+
+ def updateProfilesIni(self, profilePath):
+ # update profiles.ini on the device to point to the test profile
+ self.originalProfilesIni = tempfile.mktemp()
+ self._devicemanager.getFile(self.remoteProfilesIniPath, self.originalProfilesIni)
+
+ config = ProfileConfigParser()
+ config.read(self.originalProfilesIni)
+ for section in config.sections():
+ if 'Profile' in section:
+ config.set(section, 'IsRelative', 0)
+ config.set(section, 'Path', profilePath)
+
+ newProfilesIni = tempfile.mktemp()
+ with open(newProfilesIni, 'wb') as configfile:
+ config.write(configfile)
+
+ self._devicemanager.pushFile(newProfilesIni, self.remoteProfilesIniPath)
+ try:
+ os.remove(newProfilesIni)
+ except:
+ pass
+
+
+ def createReftestProfile(self, options, profileDir, reftestlist):
+ print "profileDir: " + str(profileDir)
+ retVal = RefTest.createReftestProfile(self, options, profileDir, reftestlist, server=options.remoteWebServer)
+
+ # Turn off the locale picker screen
+ fhandle = open(os.path.join(profileDir, "user.js"), 'a')
+ fhandle.write("""
+user_pref("browser.firstrun.show.localepicker", false);
+user_pref("browser.homescreenURL","app://system.gaiamobile.org");\n
+user_pref("browser.manifestURL","app://system.gaiamobile.org/manifest.webapp");\n
+user_pref("browser.tabs.remote", false);\n
+user_pref("dom.ipc.browser_frames.oop_by_default", true);\n
+user_pref("dom.ipc.tabs.disabled", false);\n
+user_pref("dom.mozBrowserFramesEnabled", true);\n
+user_pref("dom.mozBrowserFramesWhitelist","app://system.gaiamobile.org");\n
+user_pref("network.dns.localDomains","app://system.gaiamobile.org");\n
+user_pref("font.size.inflation.emPerLine", 0);
+user_pref("font.size.inflation.minTwips", 0);
+user_pref("reftest.browser.iframe.enabled", false);
+user_pref("reftest.remote", true);
+user_pref("reftest.uri", "%s");
+// Set a future policy version to avoid the telemetry prompt.
+user_pref("toolkit.telemetry.prompted", 999);
+user_pref("toolkit.telemetry.notifiedOptOut", 999);
+""" % reftestlist)
+
+ #workaround for jsreftests.
+ if getattr(options, 'enablePrivilege', False):
+ fhandle.write("""
+user_pref("capability.principal.codebase.p2.granted", "UniversalXPConnect");
+user_pref("capability.principal.codebase.p2.id", "http://%s:%s");
+""" % (options.remoteWebServer, options.httpPort))
+
+ # Close the file
+ fhandle.close()
+
+ # Copy the profile to the device.
+ self._devicemanager.removeDir(self.remoteProfile)
+ try:
+ self._devicemanager.pushDir(profileDir, self.remoteProfile)
+ except DMError:
+ print "Automation Error: Unable to copy profile to device."
+ raise
+
+ # Copy the extensions to the B2G bundles dir.
+ extensionDir = os.path.join(profileDir, 'extensions', 'staged')
+ # need to write to read-only dir
+ self._devicemanager._checkCmdAs(['remount'])
+ for filename in os.listdir(extensionDir):
+ self._devicemanager._checkCmdAs(['shell', 'rm', '-rf',
+ os.path.join(self.bundlesDir, filename)])
+ try:
+ self._devicemanager.pushDir(extensionDir, self.bundlesDir)
+ except DMError:
+ print "Automation Error: Unable to copy extensions to device."
+ raise
+
+ # In B2G, user.js is always read from /data/local, not the profile
+ # directory. Backup the original user.js first so we can restore it.
+ self._devicemanager._checkCmdAs(['shell', 'rm', '-f', '%s.orig' % self.userJS])
+ self._devicemanager._checkCmdAs(['shell', 'dd', 'if=%s' % self.userJS, 'of=%s.orig' % self.userJS])
+ self._devicemanager.pushFile(os.path.join(profileDir, "user.js"), self.userJS)
+
+ self.updateProfilesIni(self.remoteProfile)
+
+ options.profilePath = self.remoteProfile
+ return retVal
+
+ def copyExtraFilesToProfile(self, options, profileDir):
+ RefTest.copyExtraFilesToProfile(self, options, profileDir)
+ try:
+ self._devicemanager.pushDir(profileDir, options.remoteProfile)
+ except DMError:
+ print "Automation Error: Failed to copy extra files to device"
+ raise
+
+ def getManifestPath(self, path):
+ return path
+
+
+def main(args=sys.argv[1:]):
+ auto = B2GRemoteAutomation(None, "fennec", context_chrome=True)
+ parser = B2GOptions(auto)
+ options, args = parser.parse_args(args)
+
+ # create our Marionette instance
+ kwargs = {}
+ if options.emulator:
+ kwargs['emulator'] = options.emulator
+ auto.setEmulator(True)
+ if options.noWindow:
+ kwargs['noWindow'] = True
+ if options.geckoPath:
+ kwargs['gecko_path'] = options.geckoPath
+ if options.logcat_dir:
+ kwargs['logcat_dir'] = options.logcat_dir
+ if options.busybox:
+ kwargs['busybox'] = options.busybox
+ if options.symbolsPath:
+ kwargs['symbols_path'] = options.symbolsPath
+ if options.emulator_res:
+ kwargs['emulator_res'] = options.emulator_res
+ if options.b2gPath:
+ kwargs['homedir'] = options.b2gPath
+ if options.marionette:
+ host,port = options.marionette.split(':')
+ kwargs['host'] = host
+ kwargs['port'] = int(port)
+ marionette = Marionette.getMarionetteOrExit(**kwargs)
+ auto.marionette = marionette
+
+ # create the DeviceManager
+ kwargs = {'adbPath': options.adbPath,
+ 'deviceRoot': options.remoteTestRoot}
+ if options.deviceIP:
+ kwargs.update({'host': options.deviceIP,
+ 'port': options.devicePort})
+ dm = DeviceManagerADB(**kwargs)
+ auto.setDeviceManager(dm)
+
+ options = parser.verifyRemoteOptions(options)
+
+ if (options == None):
+ print "ERROR: Invalid options specified, use --help for a list of valid options"
+ sys.exit(1)
+
+ # TODO fix exception
+ if not options.ignoreWindowSize:
+ parts = dm.getInfo('screen')['screen'][0].split()
+ width = int(parts[0].split(':')[1])
+ height = int(parts[1].split(':')[1])
+ if (width < 1366 or height < 1050):
+ print "ERROR: Invalid screen resolution %sx%s, please adjust to 1366x1050 or higher" % (width, height)
+ return 1
+
+ auto.setProduct("b2g")
+ auto.test_script = os.path.join(SCRIPT_DIRECTORY, 'b2g_start_script.js')
+ auto.test_script_args = [options.remoteWebServer, options.httpPort]
+ auto.logFinish = "REFTEST TEST-START | Shutdown"
+
+ reftest = B2GReftest(auto, dm, options, SCRIPT_DIRECTORY)
+ options = parser.verifyCommonOptions(options, reftest)
+
+ logParent = os.path.dirname(options.remoteLogFile)
+ dm.mkDir(logParent);
+ auto.setRemoteLog(options.remoteLogFile)
+ auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
+
+ # Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside' the webroot
+ manifest = args[0]
+ if os.path.exists(os.path.join(SCRIPT_DIRECTORY, args[0])):
+ manifest = "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, args[0])
+ elif os.path.exists(args[0]):
+ manifestPath = os.path.abspath(args[0]).split(SCRIPT_DIRECTORY)[1].strip('/')
+ manifest = "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, manifestPath)
+ else:
+ print "ERROR: Could not find test manifest '%s'" % manifest
+ return 1
+
+ # Start the webserver
+ retVal = 1
+ try:
+ retVal = reftest.startWebServer(options)
+ if retVal:
+ return retVal
+ procName = options.app.split('/')[-1]
+ if (dm.processExist(procName)):
+ dm.killProcess(procName)
+
+ cmdlineArgs = ["-reftest", manifest]
+ if getattr(options, 'bootstrap', False):
+ cmdlineArgs = []
+
+ retVal = reftest.runTests(manifest, options, cmdlineArgs)
+ except:
+ print "Automation Error: Exception caught while running tests"
+ traceback.print_exc()
+ reftest.stopWebServer(options)
+ try:
+ reftest.cleanup(None)
+ except:
+ pass
+ return 1
+
+ reftest.stopWebServer(options)
+ return retVal
+
+if __name__ == "__main__":
+ sys.exit(main())
+
diff --git a/layout/tools/tests/content_dumping.html b/layout/tools/tests/content_dumping.html
new file mode 100644
index 000000000..d0312bbf2
--- /dev/null
+++ b/layout/tools/tests/content_dumping.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
+<html>
+<head>
+ <title>Control Frame</title>
+</head>
+
+<script type="application/javascript">
+
+const nsILayoutDebuggingTools = Components.interfaces.nsILayoutDebuggingTools;
+var gDebugTools;
+
+function Init()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools = Components.classes["@mozilla.org/layout-debug/layout-debuggingtools;1"].createInstance(nsILayoutDebuggingTools);
+ gDebugTools.init(window.frames.pageframe);
+}
+
+function SetShowFrameBorders(inShow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.visualDebugging = inShow;
+}
+
+function SetShowEventTargetBorders(inShow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.visualEventDebugging = inShow;
+}
+
+function SetShowReflowStats(inShow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.reflowCounts = inShow;
+}
+
+function DumpFrames()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpFrames();
+}
+
+function DumpContent()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpContent();
+}
+
+function DumpViews()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpViews();
+}
+
+function DumpWebShells()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpWebShells();
+}
+
+function InputKey(inEvent)
+{
+ if (inEvent.keyCode == KeyEvent.DOM_VK_ENTER || inEvent.keyCode == KeyEvent.DOM_VK_RETURN)
+ {
+ var pageFrame = window.frames.pageframe;
+ pageFrame.location.href = document.dumpform.urlfield.value;
+ inEvent.preventDefault(); // avoid form submit on hitting return
+ }
+}
+
+function IframeLoaded()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ document.dumpform.urlfield.value = window.frames.pageframe.location.href;
+}
+</script>
+
+<body onload="Init()">
+
+<form name="dumpform">
+<div style="margin-bottom: 5px">
+URL: <input type="text" size="100" name="urlfield" value="http://www.mozilla.org" onkeypress="InputKey(event)"></input>
+</div>
+<div>
+<input type="button" value="Dump Frames" onclick="DumpFrames()">
+<input type="button" value="Dump Content" onclick="DumpContent()">
+<input type="button" value="Dump Views" onclick="DumpViews()">
+<input type="button" value="Dump WebShells" onclick="DumpWebShells()">
+
+<input type="checkbox" id="showBordersCheck" name="showBordersCheck"
+ onchange="SetShowFrameBorders(document.dumpform.showBordersCheck.checked)"></input>
+<label for="showBordersCheck">Show Frame Borders</label>
+</div>
+</form>
+
+<iframe name="pageframe" style="border: 1px solid black; width:800px; height:800px;" onload="IframeLoaded()"></iframe>
+
+</body>
+</html>
+
diff --git a/layout/tools/tests/debug_utils.html b/layout/tools/tests/debug_utils.html
new file mode 100644
index 000000000..4c2ba98c5
--- /dev/null
+++ b/layout/tools/tests/debug_utils.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
+<html>
+<head>
+ <title>Layout Debug Utilities</title>
+</head>
+<script type="application/javascript">
+
+const nsILayoutDebuggingTools = Components.interfaces.nsILayoutDebuggingTools;
+var gDebugTools;
+
+function Init()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools = Components.classes["@mozilla.org/layout-debug/layout-debuggingtools;1"].createInstance(nsILayoutDebuggingTools);
+ gDebugTools.init(window);
+}
+
+function SetShowFrameBorders(inShow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.visualDebugging = inShow;
+}
+
+function SetShowEventTargetBorders(inShow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.visualEventDebugging = inShow;
+}
+
+function SetShowReflowStats(inShow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.reflowCounts = inShow;
+}
+
+function DumpFrames()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpFrames();
+}
+
+function DumpContent()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpContent();
+}
+
+function DumpViews()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpViews();
+}
+
+function DumpWebShells()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ gDebugTools.dumpWebShells();
+}
+
+</script>
+
+<body onload="Init()">
+<h1>Layout Debug Utils</h1>
+
+<p>Note that these only work in debug builds</h1>
+
+<h2>Global settings</h2>
+
+<form name="globalsform">
+<div>
+<input type="checkbox" id="showBordersCheck" name="showBordersCheck"
+ onchange="SetShowFrameBorders(document.globalsform.showBordersCheck.checked)"></input>
+<label for="showBordersCheck">Show Frame Borders</label>
+</div>
+<div>
+<input type="checkbox"
+ id="showEventTargetCheck"
+ name="showEventTargetCheck"
+ onchange="SetShowEventTargetBorders(document.globalsform.showEventTargetCheck.checked)"></input>
+<label for="showEventTargetCheck">Show Event Target Borders</label>
+</div>
+</form>
+
+<h2>Per-Window settings</h2>
+
+<form name="windowform">
+<input type="checkbox"
+ id="showReflowStatsCheck"
+ name="showReflowStatsCheck"
+ onchange="SetShowReflowStats(document.windowform.showReflowStatsCheck.checked)"></input>
+<label for="showReflowStatsCheck">Show Reflow Stats</label>
+</form>
+
+<h2>Dumping</h2>
+
+<form name="dumpform">
+<div>
+<input type="button" value="Dump Frames" onclick="DumpFrames()">
+<input type="button" value="Dump Content" onclick="DumpContent()">
+<input type="button" value="Dump Views" onclick="DumpViews()">
+<input type="button" value="Dump WebShells" onclick="DumpWebShells()">
+</div>
+
+</form>
+
+
+
+</body>
+</html>
+
diff --git a/layout/tools/tests/regression_tests.html b/layout/tools/tests/regression_tests.html
new file mode 100644
index 000000000..8eff8fc77
--- /dev/null
+++ b/layout/tools/tests/regression_tests.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
+<html>
+<head>
+ <title>Layout Regression Test Harness</title>
+</head>
+
+<style>
+p.note
+{
+ background-color: #FFFFDD;
+ border: 2px solid red;
+ padding: 10px;
+}
+
+div.indent
+{
+ margin-left: 20px;
+ padding: 5px;
+}
+
+#tests
+{
+ border: 1px solid black;
+ margin: 10px;
+}
+
+#results
+{
+ border: 1px solid black;
+ margin: 10px;
+ overflow: auto;
+ height: 200px;
+}
+</style>
+
+<script src="regression_tests.js" type="application/javascript">
+<!--
+
+//-->
+</script>
+
+<body onload="DoOnload();">
+
+<h1>Layout Regression Test Harness</h1>
+
+<p class="note">
+The JavaScript in this file requires that you grant it XPConnect access,
+via the dialog that appears when you first load the file. Note that the code
+herein creates directories and files, so there is the possibility that it
+may do damage to the contents of your hard disk. You have been warned!
+</p>
+
+<h2>Tests</h2>
+<div id="tests">
+<form name="testForm">
+ <div class="indent">
+ <input type="radio" name="testType" id="singleFileRadio" checked="true" onclick="UpdateRunTestsButton()"></input><label for="singleFileRadio">Single testcase</label>
+ <div class="indent">
+ URL: <input id="singleTestFileInput" name="singleTestFileInput" type="text" size="80" oninput="UpdateRunTestsButton()"></input>
+ <input type="button" onclick="ChooseTestcaseFile();" value="Choose File...">
+ </div>
+ </div>
+
+ <div class="indent">
+ <input type="radio" name="testType" id="dirsRadio" onclick="UpdateRunTestsButton()"></input><label for="dirsRadio">Local Directories</label>
+ <div class="indent">
+ <select id="testDirsSelect" size="5" style="width: 200pt">
+ <option>None selected</option>
+ </select><br>
+ <input type="button" value="Add..." onclick="AppendTestcaseDir();">
+ <input type="button" value="Remove" onclick="RemoveTestcaseDir();">
+ </div>
+ </div>
+
+ <div class="indent">
+ <hr>
+ <table cellpadding="5px">
+ <thead>
+ <tr>
+ <td><strong>Do what</strong></td>
+ <td><strong>Output file locations</strong></td>
+ </tr>
+ </thead>
+ <tr>
+ <td>
+ <div><input id="baselineRadio" type="radio" name="doWhat" onclick="UpdateRunTestsButton()" checked="true"></input><label for="baselineRadio">Baseline</label></div>
+ <div><input id="verifyRadio" type="radio" name="doWhat" onclick="UpdateRunTestsButton()"></input><label for="verifyRadio">Verify</label></div>
+ <div><input id="verifCompRadio" type="radio" name="doWhat" onclick="UpdateRunTestsButton()"></input><label for="verifCompRadio">Verify and Compare</label></div>
+ <div><input id="compRadio" type="radio" name="doWhat" onclick="UpdateRunTestsButton()"></input><label for="compRadio">Compare</label></div>
+ </td>
+ <td valign="top">
+ <table cellpadding="4px">
+ <tr>
+ <td></td>
+ <td></td>
+ <td>File extensions</td>
+ </tr>
+ <tr>
+ <td align="right">Baseline:</td>
+ <td><input id="baselineOutputDir" name="baselineOutputDir" type="text" size="40" disabled="true"></input>
+ <input type="button" onclick="gBaselineOutputDir = ChooseOutputDirectory('baselineOutputDir'); UpdateRunTestsButton();" value="Choose..."></td>
+ <td><input type="text" size="6" name="baselineFileExtension" value=".bas"></input> (like ".bas")</td>
+ </tr>
+ <tr>
+ <td align="right">Verify:</td>
+ <td><input id="verifyOutputDir" name="verifyOutputDir" type="text" size="40" disabled="true"></input>
+ <input type="button" onclick="gVerifyOutputDir = ChooseOutputDirectory('verifyOutputDir'); UpdateRunTestsButton();" value="Choose..."></td>
+ <td><input type="text" size="6" name="verifyFileExtension" value=".ver"></input>(like ".ver")</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div class="indent">
+ <hr>
+ <input type="Button" name="runTests" value="Run the Tests!" onclick="RunTests();">
+ </div>
+
+ </div>
+
+</form>
+</div>
+
+<h2>Results</h2>
+<div id="results">
+</div>
+
+</body>
+</html>
+
diff --git a/layout/tools/tests/regression_tests.js b/layout/tools/tests/regression_tests.js
new file mode 100644
index 000000000..19003b08f
--- /dev/null
+++ b/layout/tools/tests/regression_tests.js
@@ -0,0 +1,553 @@
+
+
+const nsIFilePicker = Components.interfaces.nsIFilePicker;
+const nsILayoutRegressionTester = Components.interfaces.nsILayoutRegressionTester;
+
+const kTestTypeBaseline = 1;
+const kTestTypeVerify = 2;
+const kTestTypeVerifyAndCompare = 3;
+const kTestTypeCompare = 4;
+
+const kTestSourceSingleFile = 1;
+const kTestSourceDirList = 2;
+
+var gTestcaseDirArray = new Array; // array of nsILocalFiles
+
+var gBaselineOutputDir; // nsIFile
+var gVerifyOutputDir; // nsIFile
+
+var gBaselineFileExtension; // string
+var gVerifyFileExtension; // string
+
+var gTestType; // baseline, verify, compare etc.
+
+var gTestWindow;
+var gTestURLs = new Array;
+var gTestURLsIndex;
+
+function DoOnload()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+ // clear any values that the form manager may have unhelpfully filled in
+ document.testForm.singleTestFileInput.value = "";
+ document.testForm.baselineOutputDir.value = "";
+ document.testForm.verifyOutputDir.value = "";
+
+ InitFormFromPrefs();
+
+ UpdateRunTestsButton();
+}
+
+function InitFormFromPrefs()
+{
+ // load prefs
+ try {
+ var testURL = GetStringPref("nglayout.debug.testcaseURL");
+ document.testForm.singleTestFileInput.value = testURL;
+
+ var baselineDirURL = GetStringPref("nglayout.debug.baselineDirURL");
+ gBaselineOutputDir = GetFileFromURISpec(baselineDirURL);
+ document.testForm.baselineOutputDir.value = gBaselineOutputDir.path;
+
+ var verifyDirURL = GetStringPref("nglayout.debug.verifyDirURL");
+ gVerifyOutputDir = GetFileFromURISpec(verifyDirURL);
+ document.testForm.verifyOutputDir.value = gVerifyOutputDir.path;
+
+ var dirIndex = 0;
+ while (true) // we'll throw when we reach a nonexistent pref
+ {
+ var curDir = GetStringPref("nglayout.debug.testcaseDir" + dirIndex);
+ var dirFileSpec = GetFileFromURISpec(curDir);
+ gTestcaseDirArray.push(dirFileSpec);
+ dirIndex ++;
+ }
+ }
+ catch(e)
+ {
+ }
+
+ RebuildTestDirsSelect();
+}
+
+function SaveFormToPrefs()
+{
+ SaveStringPref("nglayout.debug.testcaseURL", document.testForm.singleTestFileInput.value);
+
+ // save prefs
+ if (gBaselineOutputDir)
+ {
+ var baselineDirURL = GetURISpecFromFile(gBaselineOutputDir);
+ SaveStringPref("nglayout.debug.baselineDirURL", baselineDirURL);
+ }
+
+ if (gVerifyOutputDir)
+ {
+ var verifyDirURL = GetURISpecFromFile(gVerifyOutputDir);
+ SaveStringPref("nglayout.debug.verifyDirURL", verifyDirURL);
+ }
+
+ var dirIndex;
+ for (dirIndex = 0; dirIndex < gTestcaseDirArray.length; dirIndex ++)
+ {
+ var curURL = GetURISpecFromFile(gTestcaseDirArray[dirIndex]);
+ SaveStringPref("nglayout.debug.testcaseDir" + dirIndex, curURL);
+ }
+ try
+ {
+ // clear prefs for higher indices until we throw
+ while (1)
+ {
+ ClearPref("nglayout.debug.testcaseDir" + dirIndex);
+ }
+ }
+ catch(e)
+ {
+ }
+
+}
+
+function GetURISpecFromFile(inFile)
+{
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+ var fileHandler = ioService.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+ return fileHandler.getURLSpecFromFile(inFile);
+}
+
+function GetFileFromURISpec(uriSpec)
+{
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+ var fileHandler = ioService.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+ return fileHandler.getFileFromURLSpec(uriSpec);
+}
+
+function SaveStringPref(inPrefName, inPrefValue)
+{
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+ prefs.setCharPref(inPrefName, inPrefValue);
+}
+
+function GetStringPref(inPrefName)
+{
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+ return prefs.getCharPref(inPrefName);
+}
+
+function ClearPref(inPrefName)
+{
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+ prefs.clearUserPref(inPrefName);
+}
+
+function WriteOutput(aText, aReplace, aColorString)
+{
+ var outputDiv = document.getElementById("results");
+
+ if (aReplace)
+ ClearOutput();
+
+ var childDiv = document.createElement("div");
+ var textNode = document.createTextNode(aText);
+ childDiv.appendChild(textNode);
+ childDiv.setAttribute("style", "color: " + aColorString + ";");
+ outputDiv.appendChild(childDiv);
+}
+
+function ClearOutput()
+{
+ var outputDiv = document.getElementById("results");
+ var curChild;
+ while (curChild = outputDiv.firstChild)
+ outputDiv.removeChild(curChild);
+}
+
+// returns an nsIFile
+function PickDirectory()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+ var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ fp.init(window, "Pick a directory", nsIFilePicker.modeGetFolder);
+ var result = fp.show();
+ if (result == nsIFilePicker.returnCancel)
+ throw("User cancelled");
+
+ var chosenDir = fp.file;
+ return chosenDir;
+}
+
+
+// returns a url string
+function PickFileURL()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+ var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ fp.init(window, "Pick a directory", nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterHTML + nsIFilePicker.filterText);
+
+ var result = fp.show();
+ if (result == nsIFilePicker.returnCancel)
+ throw("User cancelled");
+
+ return fp.fileURL.spec;
+}
+
+function RebuildTestDirsSelect()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ var dirsSelect = document.getElementById("testDirsSelect");
+
+ // rebuild it from gTestcaseDirArray
+ while (dirsSelect.length)
+ dirsSelect.remove(0);
+
+ var i;
+ for (i = 0; i < gTestcaseDirArray.length; i ++)
+ {
+ var curDir = gTestcaseDirArray[i];
+
+ var optionElement = document.createElement("option");
+ var textNode = document.createTextNode(curDir.leafName);
+
+ optionElement.appendChild(textNode);
+ dirsSelect.add(optionElement, null);
+ }
+
+ UpdateRunTestsButton();
+}
+
+// set the 'single testcase' file
+function ChooseTestcaseFile()
+{
+ var dirInput = document.getElementById("singleTestFileInput");
+ dirInput.value = PickFileURL();
+
+ UpdateRunTestsButton();
+}
+
+function AppendTestcaseDir()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+ var chosenDir = PickDirectory();
+ // does the array already contain this dir?
+ var i;
+ for (i = 0; i < gTestcaseDirArray.length; i ++)
+ {
+ var curElement = gTestcaseDirArray[i];
+ if (curElement.equals(chosenDir)) // nsIFile::Equals
+ return;
+ }
+
+ gTestcaseDirArray[gTestcaseDirArray.length] = chosenDir;
+ RebuildTestDirsSelect();
+}
+
+function RemoveTestcaseDir()
+{
+ var dirsSelect = document.getElementById("testDirsSelect");
+ if (dirsSelect.selectedIndex != -1)
+ {
+ gTestcaseDirArray.splice(dirsSelect.selectedIndex, 1);
+ RebuildTestDirsSelect();
+ }
+}
+
+function InputOptionsValid()
+{
+ if (document.testForm.testType[0].checked)
+ {
+ // need a test file
+ var testcaseURL = document.testForm.singleTestFileInput.value;
+ if (testcaseURL.length == 0) return false;
+ }
+ else if (document.testForm.testType[1].checked)
+ {
+ // need at least one dir
+ if (gTestcaseDirArray.length == 0) return false;
+ }
+ else
+ return false;
+
+ return true;
+}
+
+function OutputOptionsValid()
+{
+ var testType = GetTestType();
+
+ switch (testType)
+ {
+ case kTestTypeBaseline:
+ if (!gBaselineOutputDir) return false;
+ break;
+
+ case kTestTypeVerify:
+ if (!gVerifyOutputDir) return false;
+ break;
+
+ case kTestTypeVerifyAndCompare:
+ case kTestTypeCompare:
+ if (!gBaselineOutputDir || !gVerifyOutputDir) return false;
+ break;
+ }
+
+ return true;
+}
+
+function UpdateRunTestsButton()
+{
+ var testType = GetTestType();
+ var dataValid = OutputOptionsValid();
+ if (testType != kTestTypeCompare)
+ dataValid &= InputOptionsValid();
+ document.testForm.runTests.disabled = !dataValid;
+}
+
+// returns nsIFile, sets the input value
+function ChooseOutputDirectory(inputElementID)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ var chosenDir = PickDirectory();
+
+ var inputElement = document.getElementById(inputElementID);
+ inputElement.value = chosenDir.path;
+
+ return chosenDir;
+}
+
+
+function CompareFrameDumps(testFileBasename, baselineDir, baselineExt, verifyDir, verifyExt)
+{
+ var debugObject = Components.classes["@mozilla.org/layout-debug/regressiontester;1"].createInstance(nsILayoutRegressionTester);
+
+ var baseFile = baselineDir.clone();
+ baseFile.append(testFileBasename + baselineExt);
+
+ var verifyFile = verifyDir.clone();
+ verifyFile.append(testFileBasename + verifyExt);
+
+ var filesDiffer = debugObject.compareFrameModels(baseFile, verifyFile, nsILayoutRegressionTester.COMPARE_FLAGS_BRIEF);
+ if (filesDiffer)
+ {
+ WriteOutput("Test file '" + baseFile.leafName + "' failed", false, "red");
+ }
+ else
+ {
+ WriteOutput("Test file '" + baseFile.leafName + "' passed", false, "green");
+ }
+}
+
+function DumpFrames(testWindow, testFileName, outputDir, outputFileExtension)
+{
+ var debugObject = Components.classes["@mozilla.org/layout-debug/regressiontester;1"].createInstance(nsILayoutRegressionTester);
+
+ var outputFile = outputDir.clone();
+ outputFile.append(testFileName.replace(".html", outputFileExtension));
+
+ dump("Dumping frame model for " + testFileName + " to " + outputFile.leafName + "\n");
+ var result = debugObject.dumpFrameModel(testWindow, outputFile, nsILayoutRegressionTester.DUMP_FLAGS_MASK_DEFAULT);
+ if (result != 0)
+ {
+ WriteOutput("dumpFrameModel for " + testFileName + " failed", false, "orange");
+ }
+}
+
+function LoadTestURL(testWindow, theURL)
+{
+ dump("Loading test " + theURL + "\n");
+ // we use a 1/2 second delay to give time for async reflows to happen
+ testWindow.onload = setTimeout("HandleTestWindowLoad(gTestWindow)", 1000);
+ testWindow.location.href = theURL;
+}
+
+function HandleTestWindowLoad(testWindow)
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+ var outputDir;
+ var outputFileExtension;
+ var runCompare = false;
+
+ switch (gTestType)
+ {
+ case kTestTypeBaseline:
+ outputDir = gBaselineOutputDir;
+ outputFileExtension = gBaselineFileExtension;
+ break;
+
+ case kTestTypeVerify:
+ outputDir = gVerifyOutputDir;
+ outputFileExtension = gVerifyFileExtension;
+ break;
+
+ case kTestTypeVerifyAndCompare:
+ outputDir = gVerifyOutputDir;
+ outputFileExtension = gVerifyFileExtension;
+ runCompare = true;
+ break;
+
+ case kTestTypeCompare:
+ dump("Should never get here");
+ break;
+ }
+
+ var loadedURL = testWindow.location.href;
+ var loadedFile = loadedURL.substring(loadedURL.lastIndexOf('/') + 1);
+
+ DumpFrames(testWindow, loadedFile, outputDir, outputFileExtension);
+
+ if (runCompare)
+ {
+ var testFileBasename = loadedFile.replace(".html", "");
+ CompareFrameDumps(testFileBasename, gBaselineOutputDir, gBaselineFileExtension, gVerifyOutputDir, gVerifyFileExtension);
+ }
+
+ // now fire of the next one, if we have one
+ var nextURL = gTestURLs[gTestURLsIndex++];
+ if (nextURL)
+ LoadTestURL(testWindow, nextURL);
+ else
+ testWindow.close();
+}
+
+
+function AddDirectoryEntriesToTestList(inDirFile, inRequiredExtension)
+{
+ var enumerator = inDirFile.directoryEntries;
+
+ while (enumerator.hasMoreElements())
+ {
+ var curFile = enumerator.getNext();
+ curFile = curFile.QueryInterface(Components.interfaces.nsIFile);
+
+ var leafName = curFile.leafName;
+ if (leafName.indexOf(inRequiredExtension) != -1)
+ {
+ var fileURI = GetURISpecFromFile(curFile);
+ gTestURLs.push(fileURI);
+ }
+ }
+}
+
+
+// returns an array of filenames
+function DirectoryEntriesToArray(inDirFile, inRequiredExtension)
+{
+ var fileArray = new Array;
+
+ var enumerator = inDirFile.directoryEntries;
+ while (enumerator.hasMoreElements())
+ {
+ var curFile = enumerator.getNext();
+ curFile = curFile.QueryInterface(Components.interfaces.nsIFile);
+ var leafName = curFile.leafName;
+ if (leafName.indexOf(inRequiredExtension) != -1)
+ {
+ fileArray.push(leafName);
+ }
+ }
+
+ return fileArray;
+}
+
+
+function BuildTestURLsList(testSourceType)
+{
+ // clear the array
+ gTestURLs.splice(0, gTestURLs.length);
+ gTestURLsIndex = 0;
+
+ if (testSourceType == kTestSourceSingleFile)
+ {
+ var testURL = document.testForm.singleTestFileInput.value;
+ if (testURL.substr(-5) != ".html")
+ {
+ // append /index.html if we have to
+ if (testURL.substr(-1) != "/")
+ testURL += "/";
+ testURL += "index.html";
+ }
+ gTestURLs[0] = testURL;
+ }
+ else
+ {
+ for (var i = 0; i < gTestcaseDirArray.length; i++)
+ {
+ var dirFile = gTestcaseDirArray[i]; // nsIFile for the dir
+ AddDirectoryEntriesToTestList(dirFile, ".html");
+ }
+ }
+}
+
+function CompareFilesInDir(inBaseDir, inBaseExtension, inVerifyDir, inVerifyExtension)
+{
+ var comapareFiles = DirectoryEntriesToArray(inBaseDir, inBaseExtension);
+
+ for (var i = 0; i < comapareFiles.length; i ++)
+ {
+ var curFilename = comapareFiles[i];
+ var testFileBasename = curFilename.replace(inBaseExtension, "");
+ CompareFrameDumps(testFileBasename, inBaseDir, inBaseExtension, inVerifyDir, inVerifyExtension);
+ }
+}
+
+function GetTestType()
+{
+ if (document.testForm.doWhat[0].checked)
+ return kTestTypeBaseline;
+
+ if (document.testForm.doWhat[1].checked)
+ return kTestTypeVerify;
+
+ if (document.testForm.doWhat[2].checked)
+ return kTestTypeVerifyAndCompare;
+
+ if (document.testForm.doWhat[3].checked)
+ return kTestTypeCompare;
+
+ return 0;
+}
+
+function RunTests()
+{
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+ ClearOutput();
+ SaveFormToPrefs();
+
+ var testSourceType;
+ if (document.testForm.testType[0].checked)
+ testSourceType = kTestSourceSingleFile;
+ else
+ testSourceType = kTestSourceDirList;
+
+ gTestType = GetTestType();
+
+ gBaselineFileExtension = document.testForm.baselineFileExtension.value;
+ gVerifyFileExtension = document.testForm.verifyFileExtension.value;
+
+ if (gTestType == kTestTypeCompare)
+ {
+ // to compare, we'll just run through all the files in the
+ // baseline and verify dirs, and compare those that exist in
+ // both.
+ CompareFilesInDir(gBaselineOutputDir, gBaselineFileExtension, gVerifyOutputDir, gVerifyFileExtension);
+ }
+ else
+ {
+ BuildTestURLsList(testSourceType);
+
+ gTestWindow = window.open("about:blank", "Test window",
+ "width=800,height=600,status=yes,toolbars=no");
+
+ // start the first load
+ var testURL = gTestURLs[0];
+ gTestURLsIndex = 1;
+ LoadTestURL(gTestWindow, testURL);
+ }
+
+}
+
+