diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2014-05-21 11:38:25 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2014-05-21 11:38:25 +0200 |
commit | d25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch) | |
tree | 16ec27edc7d5f83986f16236d3a36a2682a0f37e /layout/tools | |
parent | a942906574671868daf122284a9c4689e6924f74 (diff) | |
download | palemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz |
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'layout/tools')
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> +<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> +<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> +<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> +<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> +<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> +</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); + } + +} + + |