mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-01 12:57:31 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
12
layout/tools/layout-debug/application.ini
Normal file
12
layout/tools/layout-debug/application.ini
Normal file
|
|
@ -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@
|
||||
2
layout/tools/layout-debug/chrome.manifest
Normal file
2
layout/tools/layout-debug/chrome.manifest
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
content layoutdebug jar:layoutdebug.jar!/content/layoutdebug/
|
||||
locale layoutdebug en-US jar:layoutdebug.jar!/locale/en-US/layoutdebug/
|
||||
5
layout/tools/layout-debug/layoutdebug-prefs.js
Normal file
5
layout/tools/layout-debug/layoutdebug-prefs.js
Normal file
|
|
@ -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");
|
||||
9
layout/tools/layout-debug/moz.build
Normal file
9
layout/tools/layout-debug/moz.build
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; 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']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
|
||||
25
layout/tools/layout-debug/src/moz.build
Normal file
25
layout/tools/layout-debug/src/moz.build
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- Mode: python; 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',
|
||||
]
|
||||
|
||||
XPIDL_MODULE = 'layout_debug'
|
||||
|
||||
EXPORTS += [
|
||||
'nsLayoutDebugCIID.h',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'nsDebugFactory.cpp',
|
||||
'nsLayoutDebugCLH.cpp',
|
||||
'nsLayoutDebuggingTools.cpp',
|
||||
'nsRegressionTester.cpp',
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
50
layout/tools/layout-debug/src/nsDebugFactory.cpp
Normal file
50
layout/tools/layout-debug/src/nsDebugFactory.cpp
Normal file
|
|
@ -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, nullptr, nsRegressionTesterConstructor },
|
||||
{ &kNS_LAYOUT_DEBUGGINGTOOLS_CID, false, nullptr, nsLayoutDebuggingToolsConstructor },
|
||||
{ &kNS_LAYOUTDEBUGCLH_CID, false, nullptr, nsLayoutDebugCLHConstructor },
|
||||
{ nullptr }
|
||||
};
|
||||
|
||||
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 },
|
||||
{ nullptr }
|
||||
};
|
||||
|
||||
static const mozilla::Module::CategoryEntry kLayoutDebugCategories[] = {
|
||||
{ "command-line-handler", "m-layoutdebug", "@mozilla.org/commandlinehandler/general-startup;1?type=layoutdebug" },
|
||||
{ nullptr }
|
||||
};
|
||||
|
||||
static const mozilla::Module kLayoutDebugModule = {
|
||||
mozilla::Module::kVersion,
|
||||
kLayoutDebugCIDs,
|
||||
kLayoutDebugContracts,
|
||||
kLayoutDebugCategories
|
||||
};
|
||||
|
||||
NSMODULE_DEFN(nsLayoutDebugModule) = &kLayoutDebugModule;
|
||||
52
layout/tools/layout-debug/src/nsILayoutDebuggingTools.idl
Normal file
52
layout/tools/layout-debug/src/nsILayoutDebuggingTools.idl
Normal file
|
|
@ -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 mozIDOMWindow;
|
||||
|
||||
/**
|
||||
* A series of hooks into non-IDL-ized layout code to allow all the
|
||||
* layout debugging functions to be used from chrome.
|
||||
*/
|
||||
|
||||
[scriptable, uuid(f336d8d3-9721-4ad3-85d0-a7018c0a3383)]
|
||||
interface nsILayoutDebuggingTools : nsISupports
|
||||
{
|
||||
|
||||
/*
|
||||
* Initialize debugger object to act on a docshell.
|
||||
*/
|
||||
void init(in mozIDOMWindow 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();
|
||||
};
|
||||
46
layout/tools/layout-debug/src/nsILayoutRegressionTester.idl
Normal file
46
layout/tools/layout-debug/src/nsILayoutRegressionTester.idl
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* -*- 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 mozIDOMWindow;
|
||||
interface nsIFile;
|
||||
|
||||
[scriptable, uuid(0a065e41-3a55-4b5c-bb41-1e848bb2d10c)]
|
||||
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 mozIDOMWindow 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);
|
||||
};
|
||||
|
||||
24
layout/tools/layout-debug/src/nsLayoutDebugCIID.h
Normal file
24
layout/tools/layout-debug/src/nsLayoutDebugCIID.h
Normal file
|
|
@ -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__
|
||||
|
||||
82
layout/tools/layout-debug/src/nsLayoutDebugCLH.cpp
Normal file
82
layout/tools/layout-debug/src/nsLayoutDebugCLH.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/* -*- 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 "nsArray.h"
|
||||
#include "nsString.h"
|
||||
#include "plstr.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsIWindowWatcher.h"
|
||||
#include "nsIServiceManager.h"
|
||||
#include "nsIDOMWindow.h"
|
||||
#include "nsISupportsPrimitives.h"
|
||||
#include "nsICommandLine.h"
|
||||
|
||||
nsLayoutDebugCLH::nsLayoutDebugCLH()
|
||||
{
|
||||
}
|
||||
|
||||
nsLayoutDebugCLH::~nsLayoutDebugCLH()
|
||||
{
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(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<nsIMutableArray> argsArray = nsArray::Create();
|
||||
|
||||
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, /*weak =*/ false);
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIWindowWatcher> wwatch =
|
||||
do_GetService(NS_WINDOWWATCHER_CONTRACTID);
|
||||
NS_ENSURE_TRUE(wwatch, NS_ERROR_FAILURE);
|
||||
|
||||
nsCOMPtr<mozIDOMWindowProxy> 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.AssignLiteral(" -layoutdebug [<url>] Start with Layout Debugger\n");
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
29
layout/tools/layout-debug/src/nsLayoutDebugCLH.h
Normal file
29
layout/tools/layout-debug/src/nsLayoutDebugCLH.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* -*- 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();
|
||||
|
||||
NS_DECL_ISUPPORTS
|
||||
NS_DECL_NSICOMMANDLINEHANDLER
|
||||
|
||||
protected:
|
||||
virtual ~nsLayoutDebugCLH();
|
||||
};
|
||||
|
||||
#endif /* !defined(nsLayoutDebugCLH_h_) */
|
||||
543
layout/tools/layout-debug/src/nsLayoutDebuggingTools.cpp
Normal file
543
layout/tools/layout-debug/src/nsLayoutDebuggingTools.cpp
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
/* -*- 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_ISUPPORTS(nsLayoutDebuggingTools, nsILayoutDebuggingTools)
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsLayoutDebuggingTools::Init(mozIDOMWindow* aWin)
|
||||
{
|
||||
if (!Preferences::GetService()) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
{
|
||||
if (!aWin)
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
auto* window = nsPIDOMWindowInner::From(aWin);
|
||||
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;
|
||||
aShellItem->GetChildCount(&n);
|
||||
for (i = 0; i < n; ++i) {
|
||||
nsCOMPtr<nsIDocShellTreeItem> child;
|
||||
aShellItem->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);
|
||||
}
|
||||
}
|
||||
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));
|
||||
RefPtr<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()
|
||||
{
|
||||
RefPtr<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;
|
||||
}
|
||||
44
layout/tools/layout-debug/src/nsLayoutDebuggingTools.h
Normal file
44
layout/tools/layout-debug/src/nsLayoutDebuggingTools.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* -*- 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 nsLayoutDebuggingTools_h
|
||||
#define nsLayoutDebuggingTools_h
|
||||
|
||||
#include "nsILayoutDebuggingTools.h"
|
||||
#include "nsIDocShell.h"
|
||||
#include "nsCOMPtr.h"
|
||||
|
||||
class nsLayoutDebuggingTools : public nsILayoutDebuggingTools {
|
||||
|
||||
public:
|
||||
nsLayoutDebuggingTools();
|
||||
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
NS_DECL_NSILAYOUTDEBUGGINGTOOLS
|
||||
|
||||
protected:
|
||||
virtual ~nsLayoutDebuggingTools();
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
#endif
|
||||
138
layout/tools/layout-debug/src/nsRegressionTester.cpp
Normal file
138
layout/tools/layout-debug/src/nsRegressionTester.cpp
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/* -*- 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 "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 "nsIFile.h"
|
||||
#include "nsViewManager.h"
|
||||
#include "nsView.h"
|
||||
|
||||
|
||||
|
||||
static NS_DEFINE_CID(kFrameUtilCID, NS_FRAME_UTIL_CID);
|
||||
|
||||
|
||||
nsRegressionTester::nsRegressionTester()
|
||||
{
|
||||
}
|
||||
|
||||
nsRegressionTester::~nsRegressionTester()
|
||||
{
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(nsRegressionTester, nsILayoutRegressionTester)
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsRegressionTester::DumpFrameModel(mozIDOMWindow* 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;
|
||||
|
||||
auto* windowToDump = nsPIDOMWindowInner::From(aWindowToDump);
|
||||
|
||||
nsCOMPtr<nsIDocShell> docShell = windowToDump->GetDocShell();
|
||||
|
||||
// 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;
|
||||
}
|
||||
32
layout/tools/layout-debug/src/nsRegressionTester.h
Normal file
32
layout/tools/layout-debug/src/nsRegressionTester.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* -*- 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"
|
||||
|
||||
//*****************************************************************************
|
||||
//*** nsRegressionTester
|
||||
//*****************************************************************************
|
||||
class nsRegressionTester : public nsILayoutRegressionTester
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
NS_DECL_NSILAYOUTREGRESSIONTESTER
|
||||
|
||||
nsRegressionTester();
|
||||
|
||||
protected:
|
||||
virtual ~nsRegressionTester();
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif /* nsRegressionTester_h__ */
|
||||
|
|
@ -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);
|
||||
}
|
||||
5
layout/tools/layout-debug/tests/unit/xpcshell.ini
Normal file
5
layout/tools/layout-debug/tests/unit/xpcshell.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[DEFAULT]
|
||||
head =
|
||||
tail =
|
||||
|
||||
[test_componentsRegistered.js]
|
||||
38
layout/tools/layout-debug/ui/content/layoutdebug-overlay.xul
Normal file
38
layout/tools/layout-debug/ui/content/layoutdebug-overlay.xul
Normal file
|
|
@ -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>
|
||||
434
layout/tools/layout-debug/ui/content/layoutdebug.js
Normal file
434
layout/tools/layout-debug/ui/content/layoutdebug.js
Normal file
|
|
@ -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, 0o777)
|
||||
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
|
||||
}
|
||||
187
layout/tools/layout-debug/ui/content/layoutdebug.xul
Normal file
187
layout/tools/layout-debug/ui/content/layoutdebug.xul
Normal file
|
|
@ -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>
|
||||
14
layout/tools/layout-debug/ui/jar.mn
Normal file
14
layout/tools/layout-debug/ui/jar.mn
Normal file
|
|
@ -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)
|
||||
|
|
@ -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">
|
||||
76
layout/tools/layout-debug/ui/locale/en-US/layoutdebug.dtd
Normal file
76
layout/tools/layout-debug/ui/locale/en-US/layoutdebug.dtd
Normal file
|
|
@ -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">
|
||||
7
layout/tools/layout-debug/ui/moz.build
Normal file
7
layout/tools/layout-debug/ui/moz.build
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- Mode: python; 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
4
layout/tools/recording/jar.mn
Normal file
4
layout/tools/recording/jar.mn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
recording.jar:
|
||||
% content recording %content/
|
||||
content/recording.xul (recording.xul)
|
||||
content/recording.js (recording.js)
|
||||
12
layout/tools/recording/moz.build
Normal file
12
layout/tools/recording/moz.build
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# -*- Mode: python; 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/.
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'recording-cmdline.js',
|
||||
'recording-cmdline.manifest',
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
74
layout/tools/recording/recording-cmdline.js
Normal file
74
layout/tools/recording/recording-cmdline.js
Normal file
|
|
@ -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]);
|
||||
3
layout/tools/recording/recording-cmdline.manifest
Normal file
3
layout/tools/recording/recording-cmdline.manifest
Normal file
|
|
@ -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
|
||||
47
layout/tools/recording/recording.js
Normal file
47
layout/tools/recording/recording.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
|
||||
|
||||
var CC = Components.classes;
|
||||
const CI = Components.interfaces;
|
||||
|
||||
const NS_GFXINFO_CONTRACTID = "@mozilla.org/gfx/info;1";
|
||||
|
||||
var gContainingWindow = null;
|
||||
|
||||
var gBrowser;
|
||||
|
||||
function OnDocumentLoad(evt) {
|
||||
if (evt.target != gBrowser.contentDocument || evt.target.location == "about:blank")
|
||||
return;
|
||||
gBrowser.removeEventListener("load", OnDocumentLoad, true);
|
||||
gContainingWindow.close();
|
||||
}
|
||||
|
||||
this.OnRecordingLoad = function OnRecordingLoad(win) {
|
||||
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 + "\n");
|
||||
if (info.AzureContentBackend == "none") {
|
||||
alert("Page recordings may only be made with Azure content enabled.");
|
||||
gContainingWindow.close();
|
||||
return;
|
||||
}
|
||||
|
||||
gBrowser.addEventListener("load", OnDocumentLoad, true);
|
||||
|
||||
var args = window.arguments[0].wrappedJSObject;
|
||||
|
||||
gBrowser.loadURI(args.uri);
|
||||
};
|
||||
13
layout/tools/recording/recording.xul
Normal file
13
layout/tools/recording/recording.xul
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!-- 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/. -->
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="recording-window"
|
||||
hidechrome="true"
|
||||
onload="OnRecordingLoad();"
|
||||
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>
|
||||
18
layout/tools/reftest/Makefile.in
Normal file
18
layout/tools/reftest/Makefile.in
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# 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/.
|
||||
|
||||
_DEST_DIR = $(DEPTH)/_tests/reftest
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
# We're installing to _tests/reftest
|
||||
TARGET_DEPTH = ../..
|
||||
include $(topsrcdir)/build/automation-build.mk
|
||||
|
||||
# copy harness and the reftest extension bits to $(_DEST_DIR)
|
||||
# This needs to happen after jar.mn handling from rules.mk included above.
|
||||
# The order of the :: rules ensures that.
|
||||
libs::
|
||||
(cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - reftest) | (cd $(_DEST_DIR) && tar -xf -)
|
||||
617
layout/tools/reftest/README.txt
Normal file
617
layout/tools/reftest/README.txt
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
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.
|
||||
|
||||
Images of the display of both tests are captured, and most test types
|
||||
involve comparing these images (e.g., test types == or !=) to determine
|
||||
whether the test passed. The captures of the tests are taken in a
|
||||
viewport that is 800 pixels wide and 1000 pixels tall, so any content
|
||||
outside that area will be ignored (except for any scrollbars that are
|
||||
displayed). Ideally, however, tests should be written so that they fit
|
||||
within 600x600, since we may in the future want to switch to 600x600 to
|
||||
match http://lists.w3.org/Archives/Public/www-style/2012Sep/0562.html .
|
||||
|
||||
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: class="reftest-wait"
|
||||
========================================
|
||||
|
||||
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: MozReftestInvalidate Event
|
||||
==============================================
|
||||
|
||||
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: class="reftest-no-paint"
|
||||
========================================
|
||||
|
||||
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.
|
||||
|
||||
Opaque Layer Tests: class="reftest-opaque-layer"
|
||||
================================================
|
||||
|
||||
If an element should be assigned to a PaintedLayer that's opaque, set the class
|
||||
"reftest-opaque-layer" on it. This checks whether the layer is opaque during
|
||||
the last paint of the test, and it works whether your test is an invalidation
|
||||
test or not. In order to pass the test, the element has to have a primary
|
||||
frame, and that frame's display items must all be assigned to a single painted
|
||||
layer and no other layers, so it can't be used on elements that create stacking
|
||||
contexts (active or inactive).
|
||||
|
||||
Layerization Tests: reftest-assigned-layer="layer-name"
|
||||
=======================================================
|
||||
|
||||
If two elements should be assigned to the same PaintedLayer, choose any string
|
||||
value as the layer name and set the attribute reftest-assigned-layer="yourname"
|
||||
on both elements. Reftest will check whether all elements with the same
|
||||
reftest-assigned-layer value share the same layer. It will also test whether
|
||||
elements with different reftest-assigned-layer values are assigned to different
|
||||
layers.
|
||||
The same restrictions as with class="reftest-opaque-layer" apply: All elements
|
||||
must have a primary frame, and that frame's display items must all be assigned
|
||||
to the same PaintedLayer and no other layers. If these requirements are not
|
||||
met, the test will fail.
|
||||
|
||||
Snapshot The Whole Window: class="reftest-snapshot-all"
|
||||
=======================================================
|
||||
|
||||
In a reftest-wait test, to disable testing of invalidation and force the final
|
||||
snapshot to be taken of the whole window, set the "reftest-snapshot-all"
|
||||
class on the root element.
|
||||
|
||||
Avoid triggering flushes: class="reftest-no-flush"
|
||||
==================================================
|
||||
|
||||
The reftest harness normally triggers flushes by calling
|
||||
getBoundingClientRect on the root element. If the root element of the
|
||||
test has class="reftest-no-flush", it doesn't do this.
|
||||
|
||||
This is useful for testing animations on the compositor thread, since
|
||||
the flushing will cause a main thread style update.
|
||||
|
||||
Zoom Tests: reftest-zoom="<float>"
|
||||
==================================
|
||||
|
||||
When the root element of a test has a "reftest-zoom" attribute, that zoom
|
||||
factor is applied when rendering the test. The corresponds to the desktop "full
|
||||
zoom" style zoom. 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.
|
||||
|
||||
Setting Scrollport Size: reftest-scrollport-w/h="<int>"
|
||||
=======================================================
|
||||
|
||||
If either of the "reftest-scrollport-w" and "reftest-scrollport-h" attributes on
|
||||
the root element are non-zero, sets the scroll-position-clamping scroll-port
|
||||
size to the given size in CSS pixels. This does not affect the size of the
|
||||
snapshot that is taken.
|
||||
|
||||
Setting Async Scroll Mode: reftest-async-scroll attribute
|
||||
=========================================================
|
||||
|
||||
If the "reftest-async-scroll" attribute is set on the root element, we try to
|
||||
enable async scrolling and zooming for the document. This is unsupported in many
|
||||
configurations.
|
||||
|
||||
Setting Displayport Dimensions: reftest-displayport-x/y/w/h="<int>"
|
||||
===================================================================
|
||||
|
||||
If any of the "reftest-displayport-x", "reftest-displayport-y",
|
||||
"reftest-displayport-w" and "reftest-displayport-h" attributes on the root
|
||||
element are nonzero, sets the displayport dimensions to the given bounds in
|
||||
CSS pixels. This does not affect the size of the snapshot that is taken.
|
||||
|
||||
When the "reftest-async-scroll" attribute is set on the root element, *all*
|
||||
elements in the document are checked for "reftest-displayport-x/y/w/h" and have
|
||||
displayports set on them when those attributes are present.
|
||||
|
||||
Testing Async Scrolling: reftest-async-scroll-x/y="<int>"
|
||||
=========================================================
|
||||
|
||||
When the "reftest-async-scroll" attribute is set on the root element, for any
|
||||
element where either the "reftest-async-scroll-x" or "reftest-async-scroll-y
|
||||
attributes are nonzero, at the end of the test take the snapshot with the given
|
||||
offset (in CSS pixels) added to the async scroll offset.
|
||||
|
||||
Testing Async Zooming: reftest-async-zoom="<float>"
|
||||
=========================================================
|
||||
|
||||
When the "reftest-async-zoom" attribute is present on the root element then at
|
||||
the end of the test take the snapshot with the given async zoom on top of any
|
||||
existing zoom. Content is not re-rendered at the new zoom level. This
|
||||
corresponds to the mobile style "pinch zoom" style of zoom. This is unsupported
|
||||
in many configurations, and any tests using this will probably want to have
|
||||
pref(apz.allow_zooming,true) on them.
|
||||
|
||||
Printing Tests: class="reftest-print"
|
||||
=====================================
|
||||
|
||||
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. Fixing the font size is suggested, although not
|
||||
required, because the pages are a fixed size in inches. The switch to page mode
|
||||
happens on load if the reftest-wait class is not present; otherwise it happens
|
||||
immediately after firing the MozReftestInvalidate event.
|
||||
|
||||
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: class="reftest-expect-process-crash"
|
||||
========================================================================
|
||||
|
||||
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.
|
||||
|
||||
Skip Forcing A Content Process Layer-Tree Update: reftest-no-sync-layers attribute
|
||||
==================================================================================
|
||||
|
||||
Normally when an multi-process reftest test ends, we force the content process
|
||||
to push a layer-tree update to the compositor before taking the snapshot.
|
||||
Setting the "reftest-no-sync-layers" attribute on the root element skips this
|
||||
step, enabling testing that layer-tree updates are being correctly generated.
|
||||
However the test must manually wait for a MozAfterPaint event before ending.
|
||||
57
layout/tools/reftest/b2g_start_script.js
Normal file
57
layout/tools/reftest/b2g_start_script.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
|
||||
|
||||
function setPermissions() {
|
||||
if (__webDriverArguments.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let serverAddr = __webDriverArguments[0];
|
||||
let serverPort = __webDriverArguments[1];
|
||||
let perms = Cc["@mozilla.org/permissionmanager;1"]
|
||||
.getService(Ci.nsIPermissionManager);
|
||||
let ioService = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let uri = ioService.newURI("http://" + serverAddr + ":" + serverPort, null, null);
|
||||
perms.add(uri, "allowXULXBL", Ci.nsIPermissionManager.ALLOW_ACTION);
|
||||
}
|
||||
|
||||
var cm = Cc["@mozilla.org/categorymanager;1"]
|
||||
.getService(Ci.nsICategoryManager);
|
||||
|
||||
// Disable update timers that cause b2g failures.
|
||||
if (cm) {
|
||||
cm.deleteCategoryEntry("update-timer", "nsUpdateService", false);
|
||||
}
|
||||
|
||||
// Load into any existing windows
|
||||
var wm = Cc["@mozilla.org/appshell/window-mediator;1"]
|
||||
.getService(Ci.nsIWindowMediator);
|
||||
var win = wm.getMostRecentWindow('');
|
||||
|
||||
setPermissions();
|
||||
|
||||
// Loading this into the global namespace causes intermittent failures.
|
||||
// See bug 882888 for more details.
|
||||
var reftest = {};
|
||||
Cu.import("chrome://reftest/content/reftest.jsm", reftest);
|
||||
|
||||
// Prevent display off during testing.
|
||||
navigator.mozPower.screenEnabled = true;
|
||||
var settingLock = navigator.mozSettings.createLock();
|
||||
var settingResult = settingLock.set({
|
||||
'screen.timeout': 0
|
||||
});
|
||||
settingResult.onsuccess = function () {
|
||||
dump("Set screen.time to 0\n");
|
||||
// Start the reftests
|
||||
reftest.OnRefTestLoad(win);
|
||||
}
|
||||
settingResult.onerror = function () {
|
||||
dump("Change screen.time failed\n");
|
||||
// Start the reftests
|
||||
reftest.OnRefTestLoad(win);
|
||||
}
|
||||
83
layout/tools/reftest/bootstrap.js
vendored
Normal file
83
layout/tools/reftest/bootstrap.js
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* 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 { utils: Cu, interfaces: Ci, classes: Cc, manager: Cm } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
var WindowListener = {
|
||||
onOpenWindow: function(win) {
|
||||
Services.wm.removeListener(WindowListener);
|
||||
|
||||
win = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
|
||||
win.addEventListener("load", function listener() {
|
||||
win.removeEventListener("load", listener, false);
|
||||
|
||||
// Load into any existing windows.
|
||||
let windows = Services.wm.getEnumerator("navigator:browser");
|
||||
while (windows.hasMoreElements()) {
|
||||
win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
|
||||
break;
|
||||
}
|
||||
|
||||
Cu.import("chrome://reftest/content/reftest.jsm");
|
||||
win.addEventListener("pageshow", function listener() {
|
||||
win.removeEventListener("pageshow", listener);
|
||||
// Add setTimeout here because windows.innerWidth/Height are not set yet.
|
||||
win.setTimeout(function() {OnRefTestLoad(win);}, 0);
|
||||
});
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
|
||||
function startup(data, reason) {
|
||||
// b2g is bootstrapped by b2g_start_script.js
|
||||
if (Services.appinfo.widgetToolkit == "gonk") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Services.appinfo.OS == "Android") {
|
||||
Cm.addBootstrappedManifestLocation(data.installPath);
|
||||
Services.wm.addListener(WindowListener);
|
||||
return;
|
||||
}
|
||||
|
||||
let orig = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
|
||||
let ios = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService2);
|
||||
ios.manageOfflineStatus = false;
|
||||
ios.offline = false;
|
||||
|
||||
let wwatch = Cc["@mozilla.org/embedcomp/window-watcher;1"]
|
||||
.getService(Ci.nsIWindowWatcher);
|
||||
let dummy = wwatch.openWindow(null, "about:blank", "dummy",
|
||||
"chrome,dialog=no,left=800,height=200,width=200,all",null);
|
||||
dummy.onload = function() {
|
||||
// Close pre-existing window
|
||||
orig.close();
|
||||
|
||||
dummy.focus();
|
||||
wwatch.openWindow(null, "chrome://reftest/content/reftest.xul", "_blank",
|
||||
"chrome,dialog=no,all", {});
|
||||
};
|
||||
}
|
||||
|
||||
function shutdown(data, reason) {
|
||||
if (Services.appinfo.widgetToolkit == "gonk") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Services.appinfo.OS == "Android") {
|
||||
Services.wm.removeListener(WindowListener);
|
||||
Cm.removedBootstrappedManifestLocation(data.installPath);
|
||||
OnRefTestUnload();
|
||||
Cu.unload("chrome://reftest/content/reftest.jsm");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function install(data, reason) {}
|
||||
function uninstall(data, reason) {}
|
||||
38
layout/tools/reftest/clean-reftest-output.pl
Normal file
38
layout/tools/reftest/clean-reftest-output.pl
Normal file
|
|
@ -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
|
||||
;
|
||||
60
layout/tools/reftest/gaia_lock_screen.js
Normal file
60
layout/tools/reftest/gaia_lock_screen.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* 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/. */
|
||||
|
||||
// NOTE: This code was forked from:
|
||||
// https://github.com/mozilla-b2g/gaia/blob/master/tests/atoms/gaia_lock_screen.js
|
||||
|
||||
'use strict';
|
||||
/* globals waitFor, finish */
|
||||
/* exported GaiaLockScreen */
|
||||
|
||||
var GaiaLockScreen = {
|
||||
|
||||
unlock: function(forcibly) {
|
||||
let setlock = window.wrappedJSObject.SettingsListener.getSettingsLock();
|
||||
let service = window.wrappedJSObject.Service;
|
||||
let obj = {'screen.timeout': 0};
|
||||
setlock.set(obj);
|
||||
|
||||
waitFor(
|
||||
function() {
|
||||
service.request('unlock', { forcibly: forcibly });
|
||||
waitFor(
|
||||
function() {
|
||||
finish(service.locked);
|
||||
},
|
||||
function() {
|
||||
return !service.locked;
|
||||
}
|
||||
);
|
||||
},
|
||||
function() {
|
||||
return !!service;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
lock: function(forcibly) {
|
||||
let service = window.wrappedJSObject.Service;
|
||||
let setlock = window.wrappedJSObject.SettingsListener.getSettingsLock();
|
||||
let obj = {'screen.timeout': 0};
|
||||
setlock.set(obj);
|
||||
waitFor(
|
||||
function() {
|
||||
service.request('lock', { forcibly: forcibly });
|
||||
waitFor(
|
||||
function() {
|
||||
finish(!service.locked);
|
||||
},
|
||||
function() {
|
||||
return service.locked;
|
||||
}
|
||||
);
|
||||
},
|
||||
function() {
|
||||
return !!service;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
22
layout/tools/reftest/install.rdf
Normal file
22
layout/tools/reftest/install.rdf
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?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>
|
||||
<em:version>1.0</em:version>
|
||||
<em:type>2</em:type>
|
||||
<em:bootstrap>true</em:bootstrap>
|
||||
<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>
|
||||
7
layout/tools/reftest/jar.mn
Normal file
7
layout/tools/reftest/jar.mn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
reftest.jar:
|
||||
% content reftest %content/
|
||||
* content/reftest-content.js (reftest-content.js)
|
||||
content/httpd.jsm (../../../netwerk/test/httpserver/httpd.js)
|
||||
content/StructuredLog.jsm (../../../testing/modules/StructuredLog.jsm)
|
||||
* content/reftest.jsm (reftest.jsm)
|
||||
content/reftest.xul (reftest.xul)
|
||||
393
layout/tools/reftest/mach_commands.py
Normal file
393
layout/tools/reftest/mach_commands.py
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
# 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 absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
import which
|
||||
from argparse import Namespace
|
||||
|
||||
from mozbuild.base import (
|
||||
MachCommandBase,
|
||||
MachCommandConditions as conditions,
|
||||
MozbuildObject,
|
||||
)
|
||||
|
||||
from mach.decorators import (
|
||||
CommandProvider,
|
||||
Command,
|
||||
)
|
||||
|
||||
import reftestcommandline
|
||||
|
||||
ADB_NOT_FOUND = '''
|
||||
The %s command requires the adb binary to be on your path.
|
||||
|
||||
If you have a B2G build, this can be found in
|
||||
'%s/out/host/<platform>/bin'.
|
||||
'''.lstrip()
|
||||
|
||||
GAIA_PROFILE_NOT_FOUND = '''
|
||||
The reftest command requires a non-debug gaia profile on Mulet.
|
||||
Either pass in --profile, or set the GAIA_PROFILE environment variable.
|
||||
|
||||
If you do not have a non-debug gaia profile, you can build one:
|
||||
$ git clone https://github.com/mozilla-b2g/gaia
|
||||
$ cd gaia
|
||||
$ make
|
||||
|
||||
The profile should be generated in a directory called 'profile'.
|
||||
'''.lstrip()
|
||||
|
||||
GAIA_PROFILE_IS_DEBUG = '''
|
||||
The reftest command requires a non-debug gaia profile on Mulet.
|
||||
The specified profile, %s, is a debug profile.
|
||||
|
||||
If you do not have a non-debug gaia profile, you can build one:
|
||||
$ git clone https://github.com/mozilla-b2g/gaia
|
||||
$ cd gaia
|
||||
$ make
|
||||
|
||||
The profile should be generated in a directory called 'profile'.
|
||||
'''.lstrip()
|
||||
|
||||
MARIONETTE_DISABLED = '''
|
||||
The reftest command requires a marionette enabled build on Mulet.
|
||||
|
||||
Add 'ENABLE_MARIONETTE=1' to your mozconfig file and re-build the application.
|
||||
Your currently active mozconfig is %s.
|
||||
'''.lstrip()
|
||||
|
||||
parser = None
|
||||
|
||||
|
||||
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 __init__(self, *args, **kwargs):
|
||||
MozbuildObject.__init__(self, *args, **kwargs)
|
||||
|
||||
# TODO Bug 794506 remove once mach integrates with virtualenv.
|
||||
build_path = os.path.join(self.topobjdir, 'build')
|
||||
if build_path not in sys.path:
|
||||
sys.path.append(build_path)
|
||||
|
||||
self.tests_dir = os.path.join(self.topobjdir, '_tests')
|
||||
self.reftest_dir = os.path.join(self.tests_dir, 'reftest')
|
||||
|
||||
def _make_shell_string(self, s):
|
||||
return "'%s'" % re.sub("'", r"'\''", s)
|
||||
|
||||
def _setup_objdir(self, args):
|
||||
# reftest imports will happen from the objdir
|
||||
sys.path.insert(0, self.reftest_dir)
|
||||
|
||||
if not args.tests:
|
||||
test_subdir = {
|
||||
"reftest": os.path.join('layout', 'reftests'),
|
||||
"crashtest": os.path.join('layout', 'crashtest'),
|
||||
}[args.suite]
|
||||
args.tests = [test_subdir]
|
||||
|
||||
tests = os.path.join(self.reftest_dir, 'tests')
|
||||
if not os.path.isdir(tests):
|
||||
os.symlink(self.topsrcdir, tests)
|
||||
|
||||
def run_b2g_test(self, b2g_home=None, xre_path=None, **kwargs):
|
||||
"""Runs a b2g reftest.
|
||||
|
||||
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.
|
||||
|
||||
tests is a list of paths. It can be a relative path from the
|
||||
top source directory, an absolute filename, or a directory containing
|
||||
test files.
|
||||
|
||||
suite is the type of reftest to run. It can be one of ('reftest',
|
||||
'crashtest').
|
||||
"""
|
||||
args = Namespace(**kwargs)
|
||||
if args.suite not in ('reftest', 'crashtest'):
|
||||
raise Exception('None or unrecognized reftest suite type.')
|
||||
|
||||
self._setup_objdir(args)
|
||||
import runreftestb2g
|
||||
|
||||
for i, path in enumerate(args.tests):
|
||||
# Non-absolute paths are relative to the packaged directory, which
|
||||
# has an extra tests/ at the start
|
||||
if os.path.exists(os.path.abspath(path)):
|
||||
path = os.path.relpath(path, os.path.join(self.topsrcdir))
|
||||
args.tests[i] = os.path.join('tests', path)
|
||||
|
||||
try:
|
||||
which.which('adb')
|
||||
except which.WhichError:
|
||||
# TODO Find adb automatically if it isn't on the path
|
||||
raise Exception(ADB_NOT_FOUND % ('%s-remote' % args.suite, b2g_home))
|
||||
|
||||
args.b2gPath = b2g_home
|
||||
args.logdir = self.reftest_dir
|
||||
args.httpdPath = os.path.join(self.topsrcdir, 'netwerk', 'test', 'httpserver')
|
||||
args.xrePath = xre_path
|
||||
args.ignoreWindowSize = True
|
||||
|
||||
return runreftestb2g.run_test_harness(parser, args)
|
||||
|
||||
def run_mulet_test(self, **kwargs):
|
||||
"""Runs a mulet reftest."""
|
||||
args = Namespace(**kwargs)
|
||||
self._setup_objdir(args)
|
||||
|
||||
import runreftestmulet
|
||||
|
||||
if self.substs.get('ENABLE_MARIONETTE') != '1':
|
||||
print(MARIONETTE_DISABLED % self.mozconfig['path'])
|
||||
return 1
|
||||
|
||||
if not args.profile:
|
||||
gaia_profile = os.environ.get('GAIA_PROFILE')
|
||||
if not gaia_profile:
|
||||
print(GAIA_PROFILE_NOT_FOUND)
|
||||
return 1
|
||||
args.profile = gaia_profile
|
||||
|
||||
if os.path.isfile(os.path.join(args.profile, 'extensions',
|
||||
'httpd@gaiamobile.org')):
|
||||
print(GAIA_PROFILE_IS_DEBUG % args.profile)
|
||||
return 1
|
||||
|
||||
args.app = self.get_binary_path()
|
||||
args.mulet = True
|
||||
|
||||
if not args.app.endswith('-bin'):
|
||||
args.app = '%s-bin' % args.app
|
||||
if not os.path.isfile(args.app):
|
||||
args.app = args.app[:-len('-bin')]
|
||||
|
||||
return runreftestmulet.run_test_harness(parser, args)
|
||||
|
||||
def run_desktop_test(self, **kwargs):
|
||||
"""Runs a reftest, in desktop Firefox."""
|
||||
import runreftest
|
||||
|
||||
args = Namespace(**kwargs)
|
||||
if args.suite not in ('reftest', 'crashtest', 'jstestbrowser'):
|
||||
raise Exception('None or unrecognized reftest suite type.')
|
||||
|
||||
default_manifest = {
|
||||
"reftest": (self.topsrcdir, "layout", "reftests", "reftest.list"),
|
||||
"crashtest": (self.topsrcdir, "testing", "crashtest", "crashtests.list"),
|
||||
"jstestbrowser": (self.topobjdir, "dist", "test-stage", "jsreftest", "tests",
|
||||
"jstests.list")
|
||||
}
|
||||
|
||||
args.extraProfileFiles.append(os.path.join(self.topobjdir, "dist", "plugins"))
|
||||
args.symbolsPath = os.path.join(self.topobjdir, "crashreporter-symbols")
|
||||
|
||||
if not args.tests:
|
||||
args.tests = [os.path.join(*default_manifest[args.suite])]
|
||||
|
||||
if args.suite == "jstestbrowser":
|
||||
args.extraProfileFiles.append(os.path.join(self.topobjdir, "dist",
|
||||
"test-stage", "jsreftest",
|
||||
"tests", "user.js"))
|
||||
|
||||
self.log_manager.enable_unstructured()
|
||||
try:
|
||||
rv = runreftest.run_test_harness(parser, args)
|
||||
finally:
|
||||
self.log_manager.disable_unstructured()
|
||||
|
||||
return rv
|
||||
|
||||
def run_android_test(self, **kwargs):
|
||||
"""Runs a reftest, in Firefox for Android."""
|
||||
|
||||
args = Namespace(**kwargs)
|
||||
if args.suite not in ('reftest', 'crashtest', 'jstestbrowser'):
|
||||
raise Exception('None or unrecognized reftest suite type.')
|
||||
|
||||
self._setup_objdir(args)
|
||||
import remotereftest
|
||||
|
||||
default_manifest = {
|
||||
"reftest": (self.topsrcdir, "layout", "reftests", "reftest.list"),
|
||||
"crashtest": (self.topsrcdir, "testing", "crashtest", "crashtests.list"),
|
||||
"jstestbrowser": ("jsreftest", "tests", "jstests.list")
|
||||
}
|
||||
|
||||
if not args.tests:
|
||||
args.tests = [os.path.join(*default_manifest[args.suite])]
|
||||
|
||||
args.extraProfileFiles.append(
|
||||
os.path.join(self.topsrcdir, "mobile", "android", "fonts"))
|
||||
|
||||
hyphenation_path = os.path.join(self.topsrcdir, "intl", "locales")
|
||||
|
||||
for (dirpath, dirnames, filenames) in os.walk(hyphenation_path):
|
||||
for filename in filenames:
|
||||
if filename.endswith('.dic'):
|
||||
args.extraProfileFiles.append(os.path.join(dirpath, filename))
|
||||
|
||||
if not args.httpdPath:
|
||||
args.httpdPath = os.path.join(self.tests_dir, "modules")
|
||||
if not args.symbolsPath:
|
||||
args.symbolsPath = os.path.join(self.topobjdir, "crashreporter-symbols")
|
||||
if not args.xrePath:
|
||||
args.xrePath = os.environ.get("MOZ_HOST_BIN")
|
||||
if not args.app:
|
||||
args.app = self.substs["ANDROID_PACKAGE_NAME"]
|
||||
if not args.utilityPath:
|
||||
args.utilityPath = args.xrePath
|
||||
args.dm_trans = "adb"
|
||||
args.ignoreWindowSize = True
|
||||
args.printDeviceInfo = False
|
||||
|
||||
from mozrunner.devices.android_device import grant_runtime_permissions
|
||||
grant_runtime_permissions(self)
|
||||
|
||||
# A symlink and some path manipulations are required so that test
|
||||
# manifests can be found both locally and remotely (via a url)
|
||||
# using the same relative path.
|
||||
if args.suite == "jstestbrowser":
|
||||
staged_js_dir = os.path.join(self.topobjdir, "dist", "test-stage", "jsreftest")
|
||||
tests = os.path.join(self.reftest_dir, 'jsreftest')
|
||||
if not os.path.isdir(tests):
|
||||
os.symlink(staged_js_dir, tests)
|
||||
args.extraProfileFiles.append(os.path.join(staged_js_dir, "tests", "user.js"))
|
||||
else:
|
||||
tests = os.path.join(self.reftest_dir, "tests")
|
||||
if not os.path.isdir(tests):
|
||||
os.symlink(self.topsrcdir, tests)
|
||||
for i, path in enumerate(args.tests):
|
||||
# Non-absolute paths are relative to the packaged directory, which
|
||||
# has an extra tests/ at the start
|
||||
if os.path.exists(os.path.abspath(path)):
|
||||
path = os.path.relpath(path, os.path.join(self.topsrcdir))
|
||||
args.tests[i] = os.path.join('tests', path)
|
||||
|
||||
self.log_manager.enable_unstructured()
|
||||
try:
|
||||
rv = remotereftest.run_test_harness(parser, args)
|
||||
finally:
|
||||
self.log_manager.disable_unstructured()
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def process_test_objects(kwargs):
|
||||
"""|mach test| works by providing a test_objects argument, from
|
||||
which the test path must be extracted and converted into a normal
|
||||
reftest tests argument."""
|
||||
|
||||
if "test_objects" in kwargs:
|
||||
if kwargs["tests"] is None:
|
||||
kwargs["tests"] = []
|
||||
kwargs["tests"].extend(item["path"] for item in kwargs["test_objects"])
|
||||
del kwargs["test_objects"]
|
||||
|
||||
|
||||
def get_parser():
|
||||
global parser
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
build_obj = MozbuildObject.from_environment(cwd=here)
|
||||
if conditions.is_android(build_obj):
|
||||
parser = reftestcommandline.RemoteArgumentsParser()
|
||||
elif conditions.is_mulet(build_obj):
|
||||
parser = reftestcommandline.B2GArgumentParser()
|
||||
else:
|
||||
parser = reftestcommandline.DesktopArgumentsParser()
|
||||
return parser
|
||||
|
||||
|
||||
@CommandProvider
|
||||
class MachCommands(MachCommandBase):
|
||||
@Command('reftest',
|
||||
category='testing',
|
||||
description='Run reftests (layout and graphics correctness).',
|
||||
parser=get_parser)
|
||||
def run_reftest(self, **kwargs):
|
||||
kwargs["suite"] = "reftest"
|
||||
return self._run_reftest(**kwargs)
|
||||
|
||||
@Command('jstestbrowser',
|
||||
category='testing',
|
||||
description='Run js/src/tests in the browser.',
|
||||
parser=get_parser)
|
||||
def run_jstestbrowser(self, **kwargs):
|
||||
self._mach_context.commands.dispatch("build",
|
||||
self._mach_context,
|
||||
what=["stage-jstests"])
|
||||
kwargs["suite"] = "jstestbrowser"
|
||||
return self._run_reftest(**kwargs)
|
||||
|
||||
@Command('crashtest',
|
||||
category='testing',
|
||||
description='Run crashtests (Check if crashes on a page).',
|
||||
parser=get_parser)
|
||||
def run_crashtest(self, **kwargs):
|
||||
kwargs["suite"] = "crashtest"
|
||||
return self._run_reftest(**kwargs)
|
||||
|
||||
def _run_reftest(self, **kwargs):
|
||||
process_test_objects(kwargs)
|
||||
reftest = self._spawn(ReftestRunner)
|
||||
if conditions.is_android(self):
|
||||
from mozrunner.devices.android_device import verify_android_device
|
||||
verify_android_device(self, install=True, xre=True)
|
||||
return reftest.run_android_test(**kwargs)
|
||||
elif conditions.is_mulet(self):
|
||||
return reftest.run_mulet_test(**kwargs)
|
||||
return reftest.run_desktop_test(**kwargs)
|
||||
|
||||
|
||||
# TODO For now b2g commands will only work with the emulator,
|
||||
# they should be modified to work with all devices.
|
||||
def is_emulator(cls):
|
||||
"""Emulator needs to be configured."""
|
||||
return cls.device_name.startswith('emulator')
|
||||
|
||||
|
||||
@CommandProvider
|
||||
class B2GCommands(MachCommandBase):
|
||||
def __init__(self, context):
|
||||
MachCommandBase.__init__(self, context)
|
||||
|
||||
for attr in ('b2g_home', 'xre_path', 'device_name'):
|
||||
setattr(self, attr, getattr(context, attr, None))
|
||||
|
||||
@Command('reftest-remote', category='testing',
|
||||
description='Run a remote reftest (b2g layout and graphics correctness, remote device).',
|
||||
conditions=[conditions.is_b2g, is_emulator],
|
||||
parser=get_parser)
|
||||
def run_reftest_remote(self, **kwargs):
|
||||
kwargs["suite"] = "reftest"
|
||||
return self._run_reftest(**kwargs)
|
||||
|
||||
@Command('crashtest-remote', category='testing',
|
||||
description='Run a remote crashtest (Check if b2g crashes on a page, remote device).',
|
||||
conditions=[conditions.is_b2g, is_emulator],
|
||||
parser=get_parser)
|
||||
def run_crashtest_remote(self, test_file, **kwargs):
|
||||
kwargs["suite"] = "crashtest"
|
||||
return self._run_reftest(**kwargs)
|
||||
|
||||
def _run_reftest(self, **kwargs):
|
||||
process_test_objects(kwargs)
|
||||
if self.device_name:
|
||||
if self.device_name.startswith('emulator'):
|
||||
emulator = 'arm'
|
||||
if 'x86' in self.device_name:
|
||||
emulator = 'x86'
|
||||
kwargs['emulator'] = emulator
|
||||
|
||||
reftest = self._spawn(ReftestRunner)
|
||||
return reftest.run_b2g_test(self.b2g_home, self.xre_path, **kwargs)
|
||||
92
layout/tools/reftest/mach_test_package_commands.py
Normal file
92
layout/tools/reftest/mach_test_package_commands.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# 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 os
|
||||
from argparse import Namespace
|
||||
from functools import partial
|
||||
|
||||
from mach.decorators import (
|
||||
CommandProvider,
|
||||
Command,
|
||||
)
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def run_reftest(context, **kwargs):
|
||||
import mozinfo
|
||||
|
||||
args = Namespace(**kwargs)
|
||||
args.e10s = context.mozharness_config.get('e10s', args.e10s)
|
||||
|
||||
if not args.tests:
|
||||
args.tests = [os.path.join('layout', 'reftests', 'reftest.list')]
|
||||
|
||||
test_root = os.path.join(context.package_root, 'reftest', 'tests')
|
||||
normalize = partial(context.normalize_test_path, test_root)
|
||||
args.tests = map(normalize, args.tests)
|
||||
|
||||
if mozinfo.info.get('buildapp') == 'mobile/android':
|
||||
return run_reftest_android(context, args)
|
||||
return run_reftest_desktop(context, args)
|
||||
|
||||
|
||||
def run_reftest_desktop(context, args):
|
||||
from runreftest import run_test_harness
|
||||
|
||||
args.app = args.app or context.firefox_bin
|
||||
args.extraProfileFiles.append(os.path.join(context.bin_dir, 'plugins'))
|
||||
args.utilityPath = context.bin_dir
|
||||
|
||||
return run_test_harness(parser, args)
|
||||
|
||||
|
||||
def run_reftest_android(context, args):
|
||||
from remotereftest import run_test_harness
|
||||
|
||||
args.app = args.app or 'org.mozilla.fennec'
|
||||
args.utilityPath = context.hostutils
|
||||
args.xrePath = context.hostutils
|
||||
args.httpdPath = context.module_dir
|
||||
args.dm_trans = 'adb'
|
||||
args.ignoreWindowSize = True
|
||||
args.printDeviceInfo = False
|
||||
|
||||
config = context.mozharness_config
|
||||
if config:
|
||||
args.remoteWebServer = config['remote_webserver']
|
||||
args.httpPort = config['emulator']['http_port']
|
||||
args.sslPort = config['emulator']['ssl_port']
|
||||
args.adb_path = config['exes']['adb'] % {'abs_work_dir': context.mozharness_workdir}
|
||||
|
||||
return run_test_harness(parser, args)
|
||||
|
||||
|
||||
def setup_argument_parser():
|
||||
import mozinfo
|
||||
import reftestcommandline
|
||||
|
||||
global parser
|
||||
mozinfo.find_and_update_from_json(here)
|
||||
if mozinfo.info.get('buildapp') == 'mobile/android':
|
||||
parser = reftestcommandline.RemoteArgumentsParser()
|
||||
else:
|
||||
parser = reftestcommandline.DesktopArgumentsParser()
|
||||
return parser
|
||||
|
||||
|
||||
@CommandProvider
|
||||
class ReftestCommands(object):
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
@Command('reftest', category='testing',
|
||||
description='Run the reftest harness.',
|
||||
parser=setup_argument_parser)
|
||||
def reftest(self, **kwargs):
|
||||
kwargs['suite'] = 'reftest'
|
||||
return run_reftest(self.context, **kwargs)
|
||||
33
layout/tools/reftest/moz.build
Normal file
33
layout/tools/reftest/moz.build
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# -*- Mode: python; 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/.
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Testing', 'Reftest')
|
||||
|
||||
XPI_NAME = 'reftest'
|
||||
USE_EXTENSION_MANIFEST = True
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
FINAL_TARGET_PP_FILES += ['install.rdf']
|
||||
FINAL_TARGET_FILES += ['bootstrap.js']
|
||||
|
||||
GENERATED_FILES += ['automation.py']
|
||||
TEST_HARNESS_FILES.reftest += [
|
||||
'!automation.py',
|
||||
'/build/mobile/b2gautomation.py',
|
||||
'/build/mobile/remoteautomation.py',
|
||||
'/build/pgo/server-locations.txt',
|
||||
'/testing/mochitest/server.js',
|
||||
'b2g_start_script.js',
|
||||
'gaia_lock_screen.js',
|
||||
'mach_test_package_commands.py',
|
||||
'output.py',
|
||||
'reftest-preferences.js',
|
||||
'reftestcommandline.py',
|
||||
'remotereftest.py',
|
||||
'runreftest.py',
|
||||
'runreftestb2g.py',
|
||||
'runreftestmulet.py',
|
||||
]
|
||||
135
layout/tools/reftest/output.py
Normal file
135
layout/tools/reftest/output.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# 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 json
|
||||
import os
|
||||
import threading
|
||||
|
||||
from mozlog.formatters import TbplFormatter
|
||||
from mozrunner.utils import get_stack_fixer_function
|
||||
|
||||
|
||||
class ReftestFormatter(TbplFormatter):
|
||||
"""
|
||||
Formatter designed to preserve the legacy "tbpl" format in reftest.
|
||||
|
||||
This is needed for both the reftest-analyzer and mozharness log parsing.
|
||||
We can change this format when both reftest-analyzer and mozharness have
|
||||
been changed to read structured logs.
|
||||
"""
|
||||
|
||||
def __call__(self, data):
|
||||
if 'component' in data and data['component'] == 'mozleak':
|
||||
# Output from mozleak requires that no prefix be added
|
||||
# so that mozharness will pick up these failures.
|
||||
return "%s\n" % data['message']
|
||||
|
||||
formatted = TbplFormatter.__call__(self, data)
|
||||
if data['action'] == 'process_output':
|
||||
return formatted
|
||||
return 'REFTEST %s' % formatted
|
||||
|
||||
def log(self, data):
|
||||
prefix = "%s |" % data['level'].upper()
|
||||
return "%s %s\n" % (prefix, data['message'])
|
||||
|
||||
def test_end(self, data):
|
||||
extra = data.get('extra', {})
|
||||
status = data['status']
|
||||
test = data['test']
|
||||
|
||||
status_msg = "TEST-"
|
||||
if 'expected' in data:
|
||||
status_msg += "UNEXPECTED-%s" % status
|
||||
else:
|
||||
if status != "PASS":
|
||||
status_msg += "KNOWN-"
|
||||
status_msg += status
|
||||
if extra.get('status_msg') == 'Random':
|
||||
status_msg += "(EXPECTED RANDOM)"
|
||||
|
||||
|
||||
output_text = "%s | %s | %s" % (status_msg, test, data.get("message", ""))
|
||||
|
||||
if "reftest_screenshots" in extra:
|
||||
screenshots = extra["reftest_screenshots"]
|
||||
if len(screenshots) == 3:
|
||||
output_text += ("\nREFTEST IMAGE 1 (TEST): data:image/png;base64,%s\n"
|
||||
"REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,%s") % (screenshots[0]["screenshot"],
|
||||
screenshots[2]["screenshot"])
|
||||
elif len(screenshots) == 1:
|
||||
output_text += "\nREFTEST IMAGE: data:image/png;base64,%(image1)s" % screenshots[0]["screenshot"]
|
||||
|
||||
|
||||
output_text += "\nREFTEST TEST-END | %s" % test
|
||||
return "%s\n" % output_text
|
||||
|
||||
def process_output(self, data):
|
||||
return "%s\n" % data["data"]
|
||||
|
||||
def suite_end(self, data):
|
||||
lines = []
|
||||
summary = data['extra']['results']
|
||||
summary['success'] = summary['Pass'] + summary['LoadOnly']
|
||||
lines.append("Successful: %(success)s (%(Pass)s pass, %(LoadOnly)s load only)" %
|
||||
summary)
|
||||
summary['unexpected'] = (summary['Exception'] + summary['FailedLoad'] +
|
||||
summary['UnexpectedFail'] + summary['UnexpectedPass'] +
|
||||
summary['AssertionUnexpected'] +
|
||||
summary['AssertionUnexpectedFixed'])
|
||||
lines.append(("Unexpected: %(unexpected)s (%(UnexpectedFail)s unexpected fail, "
|
||||
"%(UnexpectedPass)s unexpected pass, "
|
||||
"%(AssertionUnexpected)s unexpected asserts, "
|
||||
"%(FailedLoad)s failed load, "
|
||||
"%(Exception)s exception)") % summary)
|
||||
summary['known'] = (summary['KnownFail'] + summary['AssertionKnown'] +
|
||||
summary['Random'] + summary['Skip'] + summary['Slow'])
|
||||
lines.append(("Known problems: %(known)s (" +
|
||||
"%(KnownFail)s known fail, " +
|
||||
"%(AssertionKnown)s known asserts, " +
|
||||
"%(Random)s random, " +
|
||||
"%(Skip)s skipped, " +
|
||||
"%(Slow)s slow)") % summary)
|
||||
lines = ["REFTEST INFO | %s" % s for s in lines]
|
||||
lines.append("REFTEST SUITE-END | Shutdown")
|
||||
return "INFO | Result summary:\n{}\n".format('\n'.join(lines))
|
||||
|
||||
|
||||
class OutputHandler(object):
|
||||
"""Process the output of a process during a test run and translate
|
||||
raw data logged from reftest.js to an appropriate structured log action,
|
||||
where applicable.
|
||||
"""
|
||||
|
||||
def __init__(self, log, utilityPath, symbolsPath=None):
|
||||
self.stack_fixer_function = get_stack_fixer_function(utilityPath, symbolsPath)
|
||||
self.log = log
|
||||
# needed for b2gautomation.py
|
||||
self.suite_finished = False
|
||||
|
||||
def __call__(self, line):
|
||||
# need to return processed messages to appease remoteautomation.py
|
||||
if not line.strip():
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
except ValueError:
|
||||
self.verbatim(line)
|
||||
return [line]
|
||||
|
||||
if isinstance(data, dict) and 'action' in data:
|
||||
if data['action'] == 'suite_end':
|
||||
self.suite_finished = True
|
||||
|
||||
self.log.log_raw(data)
|
||||
else:
|
||||
self.verbatim(json.dumps(data))
|
||||
|
||||
return [data]
|
||||
|
||||
def verbatim(self, line):
|
||||
if self.stack_fixer_function:
|
||||
line = self.stack_fixer_function(line)
|
||||
self.log.process_output(threading.current_thread().name, line)
|
||||
649
layout/tools/reftest/reftest-analyzer-structured.xhtml
Normal file
649
layout/tools/reftest/reftest-analyzer-structured.xhtml
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
|
||||
<!-- vim: set shiftwidth=2 tabstop=2 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/. -->
|
||||
<!--
|
||||
|
||||
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 td.activeitem { background-color: yellow; }
|
||||
|
||||
/*
|
||||
#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
|
||||
var gParams;
|
||||
|
||||
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();
|
||||
gParams = hash_parameters();
|
||||
if (gParams.log) {
|
||||
show_phase("loading");
|
||||
process_log(gParams.log);
|
||||
} else if (gParams.logurl) {
|
||||
show_phase("loading");
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState === 4) {
|
||||
process_log(req.responseText);
|
||||
}
|
||||
};
|
||||
req.open('GET', gParams.logurl, true);
|
||||
req.send();
|
||||
}
|
||||
window.addEventListener('keypress', handle_keyboard_shortcut, 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];
|
||||
try {
|
||||
var data = JSON.parse(line);
|
||||
} catch(e) {
|
||||
continue;
|
||||
}
|
||||
// Ignore duplicated output in logcat.
|
||||
if (!data.action == "test_end" && data.status != "FAIL")
|
||||
continue;
|
||||
|
||||
if (!data.hasOwnProperty("extra") ||
|
||||
!data.extra.hasOwnProperty("reftest_screenshots")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var url = data.test;
|
||||
var screenshots = data.extra.reftest_screenshots;
|
||||
gTestItems.push(
|
||||
{
|
||||
pass: data.status === "PASS",
|
||||
// only one of the following three should ever be true
|
||||
unexpected: data.hasOwnProperty("expected"),
|
||||
random: false,
|
||||
skip: data.status == "SKIP",
|
||||
url: url,
|
||||
images: [],
|
||||
imageLabels: []
|
||||
});
|
||||
|
||||
var item = gTestItems[gTestItems.length - 1];
|
||||
item.images.push("data:image/png;base64," + screenshots[0].screenshot);
|
||||
item.imageLabels.push(screenshots[0].url);
|
||||
if (screenshots.length > 1) {
|
||||
item.images.push("data:image/png;base64," + screenshots[2].screenshot);
|
||||
item.imageLabels.push(screenshots[2].url);
|
||||
}
|
||||
}
|
||||
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];
|
||||
|
||||
// optional url filter for only showing unexpected results
|
||||
if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
|
||||
continue;
|
||||
|
||||
// XXX regardless 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.id = "item" + i;
|
||||
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 = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
|
||||
};
|
||||
img.src = src;
|
||||
}
|
||||
|
||||
function sync_svg_size(imageData) {
|
||||
// We need the size of the 'svg' and its 'image' elements to match the size
|
||||
// of the ImageData objects that we're going to read pixels from or else our
|
||||
// magnify() function will be very broken.
|
||||
ID("svg").setAttribute("width", imageData.width);
|
||||
ID("svg").setAttribute("height", imageData.height);
|
||||
}
|
||||
|
||||
function show_images(i) {
|
||||
var item = gTestItems[i];
|
||||
var cell = ID("images");
|
||||
|
||||
// Remove activeitem class from any existing elements
|
||||
var activeItems = document.querySelectorAll(".activeitem");
|
||||
for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
|
||||
activeItems[activeItemIdx].classList.remove("activeitem");
|
||||
}
|
||||
|
||||
ID("item" + i).classList.add("activeitem");
|
||||
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]);
|
||||
|
||||
ID("label1").textContent = 'Image ' + item.imageLabels[0];
|
||||
ID("label2").textContent = 'Image ' + item.imageLabels[1];
|
||||
}
|
||||
|
||||
cell.style.display = "";
|
||||
|
||||
get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); });
|
||||
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 handle_keyboard_shortcut(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;
|
||||
case 100: // "d" key
|
||||
document.getElementById("differences").click();
|
||||
break;
|
||||
case 112: // "p" key
|
||||
shift_images(-1);
|
||||
break;
|
||||
case 110: // "n" key
|
||||
shift_images(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function shift_images(dir) {
|
||||
var activeItem = document.querySelector(".activeitem");
|
||||
if (!activeItem) {
|
||||
return;
|
||||
}
|
||||
for (var elm = activeItem; elm; elm = elm.parentElement) {
|
||||
if (elm.tagName != "tr") {
|
||||
continue;
|
||||
}
|
||||
elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
|
||||
if (elm) {
|
||||
elm.getElementsByTagName("a")[0].click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
// Here we just use the dimensions of gImage1Data since we expect test
|
||||
// and reference to have the same dimensions.
|
||||
if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
|
||||
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 raw structured 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">
|
||||
<input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
|
||||
<input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" /><label id="label2" title="2" for="radio2">Image 2</label>
|
||||
<label><input id="differences" 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="800" height="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>
|
||||
647
layout/tools/reftest/reftest-analyzer.xhtml
Normal file
647
layout/tools/reftest/reftest-analyzer.xhtml
Normal file
|
|
@ -0,0 +1,647 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
|
||||
<!-- vim: set shiftwidth=2 tabstop=2 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/. -->
|
||||
<!--
|
||||
|
||||
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 td.activeitem { background-color: yellow; }
|
||||
|
||||
/*
|
||||
#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
|
||||
var gParams;
|
||||
|
||||
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();
|
||||
gParams = hash_parameters();
|
||||
if (gParams.log) {
|
||||
show_phase("loading");
|
||||
process_log(gParams.log);
|
||||
} else if (gParams.logurl) {
|
||||
show_phase("loading");
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState === 4) {
|
||||
process_log(req.responseText);
|
||||
}
|
||||
};
|
||||
req.open('GET', gParams.logurl, true);
|
||||
req.send();
|
||||
}
|
||||
window.addEventListener('keypress', handle_keyboard_shortcut, 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];
|
||||
// Ignore duplicated output in logcat.
|
||||
if (line.match(/I\/Gecko.*?REFTEST/))
|
||||
continue;
|
||||
var match = line.match(/^.*?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: [],
|
||||
imageLabels: []
|
||||
});
|
||||
continue;
|
||||
}
|
||||
match = line.match(/IMAGE([^:]*): (data:.*)$/);
|
||||
if (match) {
|
||||
var item = gTestItems[gTestItems.length - 1];
|
||||
item.images.push(match[2]);
|
||||
item.imageLabels.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];
|
||||
|
||||
// optional url filter for only showing unexpected results
|
||||
if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
|
||||
continue;
|
||||
|
||||
// XXX regardless 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.id = "item" + i;
|
||||
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 = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
|
||||
};
|
||||
img.src = src;
|
||||
}
|
||||
|
||||
function sync_svg_size(imageData) {
|
||||
// We need the size of the 'svg' and its 'image' elements to match the size
|
||||
// of the ImageData objects that we're going to read pixels from or else our
|
||||
// magnify() function will be very broken.
|
||||
ID("svg").setAttribute("width", imageData.width);
|
||||
ID("svg").setAttribute("height", imageData.height);
|
||||
}
|
||||
|
||||
function show_images(i) {
|
||||
var item = gTestItems[i];
|
||||
var cell = ID("images");
|
||||
|
||||
// Remove activeitem class from any existing elements
|
||||
var activeItems = document.querySelectorAll(".activeitem");
|
||||
for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
|
||||
activeItems[activeItemIdx].classList.remove("activeitem");
|
||||
}
|
||||
|
||||
ID("item" + i).classList.add("activeitem");
|
||||
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]);
|
||||
|
||||
ID("label1").textContent = 'Image ' + item.imageLabels[0];
|
||||
ID("label2").textContent = 'Image ' + item.imageLabels[1];
|
||||
}
|
||||
|
||||
cell.style.display = "";
|
||||
|
||||
get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); });
|
||||
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 handle_keyboard_shortcut(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;
|
||||
case 100: // "d" key
|
||||
document.getElementById("differences").click();
|
||||
break;
|
||||
case 112: // "p" key
|
||||
shift_images(-1);
|
||||
break;
|
||||
case 110: // "n" key
|
||||
shift_images(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function shift_images(dir) {
|
||||
var activeItem = document.querySelector(".activeitem");
|
||||
if (!activeItem) {
|
||||
return;
|
||||
}
|
||||
for (var elm = activeItem; elm; elm = elm.parentElement) {
|
||||
if (elm.tagName != "tr") {
|
||||
continue;
|
||||
}
|
||||
elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
|
||||
if (elm) {
|
||||
elm.getElementsByTagName("a")[0].click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
// Here we just use the dimensions of gImage1Data since we expect test
|
||||
// and reference to have the same dimensions.
|
||||
if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
|
||||
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">
|
||||
<input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
|
||||
<input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" /><label id="label2" title="2" for="radio2">Image 2</label>
|
||||
<label><input id="differences" 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="800" height="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>
|
||||
1174
layout/tools/reftest/reftest-content.js
Normal file
1174
layout/tools/reftest/reftest-content.js
Normal file
File diff suppressed because it is too large
Load diff
127
layout/tools/reftest/reftest-preferences.js
Normal file
127
layout/tools/reftest/reftest-preferences.js
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// 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.
|
||||
user_pref("dom.use_xbl_scopes_for_remote_xul", false);
|
||||
user_pref("gfx.color_management.mode", 2);
|
||||
user_pref("gfx.color_management.force_srgb", true);
|
||||
user_pref("gfx.logging.level", 1);
|
||||
user_pref("browser.dom.window.dump.enabled", true);
|
||||
user_pref("ui.caretBlinkTime", -1);
|
||||
user_pref("dom.send_after_paint_to_content", true);
|
||||
// no slow script dialogs
|
||||
user_pref("dom.max_script_run_time", 0);
|
||||
user_pref("dom.max_chrome_script_run_time", 0);
|
||||
user_pref("hangmonitor.timeout", 0);
|
||||
// Ensure autoplay is enabled for all platforms.
|
||||
user_pref("media.autoplay.enabled", true);
|
||||
// Disable updates
|
||||
user_pref("app.update.enabled", false);
|
||||
user_pref("app.update.staging.enabled", false);
|
||||
user_pref("app.update.url.android", "");
|
||||
// Disable addon updates and prefetching so we don't leak them
|
||||
user_pref("extensions.update.enabled", false);
|
||||
user_pref("extensions.systemAddon.update.url", "http://localhost/dummy-system-addons.xml");
|
||||
user_pref("extensions.getAddons.cache.enabled", false);
|
||||
// Disable blocklist updates so we don't have them reported as leaks
|
||||
user_pref("extensions.blocklist.enabled", false);
|
||||
// Make url-classifier updates so rare that they won't affect tests
|
||||
user_pref("urlclassifier.updateinterval", 172800);
|
||||
// Disable downscale-during-decode, since it makes reftests more difficult.
|
||||
user_pref("image.downscale-during-decode.enabled", false);
|
||||
// Checking whether two files are the same is slow on Windows.
|
||||
// Setting this pref makes tests run much faster there.
|
||||
user_pref("security.fileuri.strict_origin_policy", false);
|
||||
// Disable the thumbnailing service
|
||||
user_pref("browser.pagethumbnails.capturing_disabled", true);
|
||||
// Since our tests are 800px wide, set the assume-designed-for width of all
|
||||
// pages to be 800px (instead of the default of 980px). This ensures that
|
||||
// in our 800px window we don't zoom out by default to try to fit the
|
||||
// assumed 980px content.
|
||||
user_pref("browser.viewport.desktopWidth", 800);
|
||||
// Disable the fade out (over time) of overlay scrollbars, since we
|
||||
// can't guarantee taking both reftest snapshots at the same point
|
||||
// during the fade.
|
||||
user_pref("layout.testing.overlay-scrollbars.always-visible", true);
|
||||
// Disable interruptible reflow since (1) it's normally not going to
|
||||
// happen, but (2) it might happen if we somehow end up with both
|
||||
// pending user events and clock skew. So to avoid having to change
|
||||
// MakeProgress to deal with waiting for interruptible reflows to
|
||||
// complete for a rare edge case, we just disable interruptible
|
||||
// reflow so that that rare edge case doesn't lead to reftest
|
||||
// failures.
|
||||
user_pref("layout.interruptible-reflow.enabled", false);
|
||||
|
||||
// Tell the search service we are running in the US. This also has the
|
||||
// desired side-effect of preventing our geoip lookup.
|
||||
user_pref("browser.search.isUS", true);
|
||||
user_pref("browser.search.countryCode", "US");
|
||||
user_pref("browser.search.geoSpecificDefaults", false);
|
||||
|
||||
// Make sure SelfSupport doesn't hit the network.
|
||||
user_pref("browser.selfsupport.url", "https://localhost/selfsupport-dummy/");
|
||||
|
||||
// use about:blank, not browser.startup.homepage
|
||||
user_pref("browser.startup.page", 0);
|
||||
|
||||
// Allow XUL and XBL files to be opened from file:// URIs
|
||||
user_pref("dom.allow_XUL_XBL_for_file", true);
|
||||
|
||||
// Allow view-source URIs to be opened from URIs that share
|
||||
// their protocol with the inner URI of the view-source URI
|
||||
user_pref("security.view-source.reachable-from-inner-protocol", true);
|
||||
|
||||
// Ensure that telemetry is disabled, so we don't connect to the telemetry
|
||||
// server in the middle of the tests.
|
||||
user_pref("toolkit.telemetry.enabled", false);
|
||||
user_pref("toolkit.telemetry.unified", false);
|
||||
// Likewise for safebrowsing.
|
||||
user_pref("browser.safebrowsing.phishing.enabled", false);
|
||||
user_pref("browser.safebrowsing.malware.enabled", false);
|
||||
user_pref("browser.safebrowsing.forbiddenURIs.enabled", false);
|
||||
user_pref("browser.safebrowsing.blockedURIs.enabled", false);
|
||||
// Likewise for tracking protection.
|
||||
user_pref("privacy.trackingprotection.enabled", false);
|
||||
user_pref("privacy.trackingprotection.pbmode.enabled", false);
|
||||
// And for snippets.
|
||||
user_pref("browser.snippets.enabled", false);
|
||||
user_pref("browser.snippets.syncPromo.enabled", false);
|
||||
user_pref("browser.snippets.firstrunHomepage.enabled", false);
|
||||
// And for useragent updates.
|
||||
user_pref("general.useragent.updates.enabled", false);
|
||||
// And for webapp updates. Yes, it is supposed to be an integer.
|
||||
user_pref("browser.webapps.checkForUpdates", 0);
|
||||
// And for about:newtab content fetch and pings.
|
||||
user_pref("browser.newtabpage.directory.source", "data:application/json,{\"reftest\":1}");
|
||||
user_pref("browser.newtabpage.directory.ping", "");
|
||||
// Only allow add-ons from the profile and app and allow foreign
|
||||
// injection
|
||||
user_pref("extensions.enabledScopes", 5);
|
||||
user_pref("extensions.autoDisableScopes", 0);
|
||||
// Allow unsigned add-ons
|
||||
user_pref("xpinstall.signatures.required", false);
|
||||
|
||||
// Don't use auto-enabled e10s
|
||||
user_pref("browser.tabs.remote.autostart.1", false);
|
||||
user_pref("browser.tabs.remote.autostart.2", false);
|
||||
|
||||
user_pref("startup.homepage_welcome_url", "");
|
||||
user_pref("startup.homepage_welcome_url.additional", "");
|
||||
user_pref("startup.homepage_override_url", "");
|
||||
|
||||
user_pref("media.gmp-manager.url.override", "http://localhost/dummy-gmp-manager.xml");
|
||||
user_pref("media.gmp-manager.updateEnabled", false);
|
||||
|
||||
// A fake bool pref for "@supports -moz-bool-pref" sanify test.
|
||||
user_pref("testing.supports.moz-bool-pref", true);
|
||||
|
||||
// Reftests load a lot of URLs very quickly. This puts avoidable and
|
||||
// unnecessary I/O pressure on the Places DB (measured to be in the
|
||||
// gigabytes).
|
||||
user_pref("places.history.enabled", false);
|
||||
|
||||
// For Firefox 52 only, ESR will support non-Flash plugins while release will
|
||||
// not, so we keep testing the non-Flash pathways
|
||||
user_pref("plugin.load_flash_only", false);
|
||||
|
||||
user_pref("media.openUnsupportedTypeWithExternalApp", false);
|
||||
118
layout/tools/reftest/reftest-to-html.pl
Normal file
118
layout/tools/reftest/reftest-to-html.pl
Normal file
|
|
@ -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
|
||||
;
|
||||
2112
layout/tools/reftest/reftest.jsm
Normal file
2112
layout/tools/reftest/reftest.jsm
Normal file
File diff suppressed because it is too large
Load diff
14
layout/tools/reftest/reftest.xul
Normal file
14
layout/tools/reftest/reftest.xul
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!-- 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/. -->
|
||||
<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.jsm" />
|
||||
<!-- The reftest browser element is dynamically created, here -->
|
||||
</window>
|
||||
132
layout/tools/reftest/reftest/__init__.py
Normal file
132
layout/tools/reftest/reftest/__init__.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# 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 os
|
||||
import re
|
||||
|
||||
RE_COMMENT = re.compile(r'\s+#')
|
||||
RE_HTTP = re.compile(r'HTTP\((\.\.(\/\.\.)*)\)')
|
||||
RE_PROTOCOL = re.compile(r'^\w+:')
|
||||
FAILURE_TYPES = (
|
||||
'fails',
|
||||
'fails-if',
|
||||
'needs-focus',
|
||||
'random',
|
||||
'random-if',
|
||||
'silentfail',
|
||||
'silentfail-if',
|
||||
'skip',
|
||||
'skip-if',
|
||||
'slow',
|
||||
'slow-if',
|
||||
'fuzzy',
|
||||
'fuzzy-if',
|
||||
'require-or',
|
||||
'asserts',
|
||||
'asserts-if',
|
||||
)
|
||||
PREF_ITEMS = (
|
||||
'pref',
|
||||
'test-pref',
|
||||
'ref-pref',
|
||||
)
|
||||
|
||||
class ReftestManifest(object):
|
||||
"""Represents a parsed reftest manifest.
|
||||
|
||||
We currently only capture file information because that is the only thing
|
||||
tools require.
|
||||
"""
|
||||
def __init__(self, finder=None):
|
||||
self.path = None
|
||||
self.dirs = set()
|
||||
self.files = set()
|
||||
self.manifests = set()
|
||||
self.tests = set()
|
||||
self.finder = finder
|
||||
|
||||
def load(self, path):
|
||||
"""Parse a reftest manifest file."""
|
||||
normalized_path = os.path.normpath(os.path.abspath(path))
|
||||
self.manifests.add(normalized_path)
|
||||
if not self.path:
|
||||
self.path = normalized_path
|
||||
|
||||
mdir = os.path.dirname(normalized_path)
|
||||
self.dirs.add(mdir)
|
||||
|
||||
if self.finder:
|
||||
lines = self.finder.get(path).read().splitlines()
|
||||
else:
|
||||
with open(path, 'r') as fh:
|
||||
lines = fh.read().splitlines()
|
||||
|
||||
urlprefix = ''
|
||||
for line in lines:
|
||||
line = line.decode('utf-8')
|
||||
|
||||
# Entire line is a comment.
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
|
||||
# Comments can begin mid line. Strip them.
|
||||
m = RE_COMMENT.search(line)
|
||||
if m:
|
||||
line = line[:m.start()]
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
items = line.split()
|
||||
tests = []
|
||||
|
||||
for i in range(len(items)):
|
||||
item = items[i]
|
||||
|
||||
if item.startswith(FAILURE_TYPES):
|
||||
continue
|
||||
if item.startswith(PREF_ITEMS):
|
||||
continue
|
||||
if item == 'HTTP':
|
||||
continue
|
||||
|
||||
m = RE_HTTP.match(item)
|
||||
if m:
|
||||
# Need to package the referenced directory.
|
||||
self.dirs.add(os.path.normpath(os.path.join(
|
||||
mdir, m.group(1))))
|
||||
continue
|
||||
|
||||
if item == 'url-prefix':
|
||||
urlprefix = items[i+1]
|
||||
break
|
||||
|
||||
if item == 'default-preferences':
|
||||
break
|
||||
|
||||
if item == 'include':
|
||||
self.load(os.path.join(mdir, items[i+1]))
|
||||
break
|
||||
|
||||
if item == 'load' or item == 'script':
|
||||
tests.append(items[i+1])
|
||||
break
|
||||
|
||||
if item == '==' or item == '!=':
|
||||
tests.extend(items[i+1:i+3])
|
||||
break
|
||||
|
||||
for f in tests:
|
||||
# We can't package about: or data: URIs.
|
||||
# Discarding data isn't correct for a parser. But retaining
|
||||
# all data isn't currently a requirement.
|
||||
if RE_PROTOCOL.match(f):
|
||||
continue
|
||||
|
||||
test = os.path.normpath(os.path.join(mdir, urlprefix + f))
|
||||
self.files.add(test)
|
||||
self.dirs.add(os.path.dirname(test))
|
||||
self.tests.add((test, normalized_path))
|
||||
749
layout/tools/reftest/reftestcommandline.py
Normal file
749
layout/tools/reftest/reftestcommandline.py
Normal file
|
|
@ -0,0 +1,749 @@
|
|||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from urlparse import urlparse
|
||||
|
||||
import mozlog
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class ReftestArgumentsParser(argparse.ArgumentParser):
|
||||
def __init__(self, **kwargs):
|
||||
super(ReftestArgumentsParser, self).__init__(**kwargs)
|
||||
|
||||
# Try to import a MozbuildObject. Success indicates that we are
|
||||
# running from a source tree. This allows some defaults to be set
|
||||
# from the source tree.
|
||||
try:
|
||||
from mozbuild.base import MozbuildObject
|
||||
self.build_obj = MozbuildObject.from_environment(cwd=here)
|
||||
except ImportError:
|
||||
self.build_obj = None
|
||||
|
||||
self.add_argument("--xre-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="xrePath",
|
||||
# individual scripts will set a sane default
|
||||
default=None,
|
||||
help="absolute path to directory containing XRE (probably xulrunner)")
|
||||
|
||||
self.add_argument("--symbols-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="symbolsPath",
|
||||
default=None,
|
||||
help="absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
|
||||
|
||||
self.add_argument("--debugger",
|
||||
action="store",
|
||||
dest="debugger",
|
||||
help="use the given debugger to launch the application")
|
||||
|
||||
self.add_argument("--debugger-args",
|
||||
action="store",
|
||||
dest="debuggerArgs",
|
||||
help="pass the given args to the debugger _before_ "
|
||||
"the application on the command line")
|
||||
|
||||
self.add_argument("--debugger-interactive",
|
||||
action="store_true",
|
||||
dest="debuggerInteractive",
|
||||
help="prevents the test harness from redirecting "
|
||||
"stdout and stderr for interactive debuggers")
|
||||
|
||||
self.add_argument("--appname",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="app",
|
||||
default=None,
|
||||
help="absolute path to application, overriding default")
|
||||
|
||||
self.add_argument("--extra-profile-file",
|
||||
action="append",
|
||||
dest="extraProfileFiles",
|
||||
default=[],
|
||||
help="copy specified files/dirs to testing profile")
|
||||
|
||||
self.add_argument("--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_argument("--leak-threshold",
|
||||
action="store",
|
||||
type=int,
|
||||
dest="defaultLeakThreshold",
|
||||
default=0,
|
||||
help="fail if the number of bytes leaked in default "
|
||||
"processes through refcounted objects (or bytes "
|
||||
"in classes with MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) "
|
||||
"is greater than the given number")
|
||||
|
||||
self.add_argument("--utility-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="utilityPath",
|
||||
default=self.build_obj.bindir if self.build_obj else None,
|
||||
help="absolute path to directory containing utility "
|
||||
"programs (xpcshell, ssltunnel, certutil)")
|
||||
|
||||
self.add_argument("--total-chunks",
|
||||
type=int,
|
||||
dest="totalChunks",
|
||||
help="how many chunks to split the tests up into")
|
||||
|
||||
self.add_argument("--this-chunk",
|
||||
type=int,
|
||||
dest="thisChunk",
|
||||
help="which chunk to run between 1 and --total-chunks")
|
||||
|
||||
self.add_argument("--log-file",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="logFile",
|
||||
default=None,
|
||||
help="file to log output to in addition to stdout")
|
||||
|
||||
self.add_argument("--skip-slow-tests",
|
||||
dest="skipSlowTests",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="skip tests marked as slow when running")
|
||||
|
||||
self.add_argument("--ignore-window-size",
|
||||
dest="ignoreWindowSize",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="ignore the window size, which may cause spurious failures and passes")
|
||||
|
||||
self.add_argument("--install-extension",
|
||||
action="append",
|
||||
dest="extensionsToInstall",
|
||||
default=[],
|
||||
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.")
|
||||
|
||||
self.add_argument("--marionette",
|
||||
default=None,
|
||||
help="host:port to use when connecting to Marionette")
|
||||
|
||||
self.add_argument("--marionette-port-timeout",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
self.add_argument("--marionette-socket-timeout",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
self.add_argument("--marionette-startup-timeout",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
self.add_argument("--setenv",
|
||||
action="append",
|
||||
type=str,
|
||||
default=[],
|
||||
dest="environment",
|
||||
metavar="NAME=VALUE",
|
||||
help="sets the given variable in the application's "
|
||||
"environment")
|
||||
|
||||
self.add_argument("--filter",
|
||||
action="store",
|
||||
type=str,
|
||||
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.")
|
||||
|
||||
self.add_argument("--shuffle",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="shuffle",
|
||||
help="run reftests in random order")
|
||||
|
||||
self.add_argument("--run-until-failure",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="runUntilFailure",
|
||||
help="stop running on the first failure. Useful for RR recordings.")
|
||||
|
||||
self.add_argument("--repeat",
|
||||
action="store",
|
||||
type=int,
|
||||
default=0,
|
||||
dest="repeat",
|
||||
help="number of times the select test(s) will be executed. Useful for "
|
||||
"finding intermittent failures.")
|
||||
|
||||
self.add_argument("--focus-filter-mode",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="focusFilterMode",
|
||||
default="all",
|
||||
help="filters tests to run by whether they require focus. "
|
||||
"Valid values are `all', `needs-focus', or `non-needs-focus'. "
|
||||
"Defaults to `all'.")
|
||||
|
||||
self.add_argument("--disable-e10s",
|
||||
action="store_false",
|
||||
default=True,
|
||||
dest="e10s",
|
||||
help="disables content processes")
|
||||
|
||||
self.add_argument("--setpref",
|
||||
action="append",
|
||||
type=str,
|
||||
default=[],
|
||||
dest="extraPrefs",
|
||||
metavar="PREF=VALUE",
|
||||
help="defines an extra user preference")
|
||||
|
||||
self.add_argument("--reftest-extension-path",
|
||||
action="store",
|
||||
dest="reftestExtensionPath",
|
||||
help="Path to the reftest extension")
|
||||
|
||||
self.add_argument("--special-powers-extension-path",
|
||||
action="store",
|
||||
dest="specialPowersExtensionPath",
|
||||
help="Path to the special powers extension")
|
||||
|
||||
self.add_argument("--suite",
|
||||
choices=["reftest", "crashtest", "jstestbrowser"],
|
||||
default=None,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
self.add_argument("--cleanup-crashes",
|
||||
action = "store_true",
|
||||
dest = "cleanupCrashes",
|
||||
default = False,
|
||||
help = "Delete pending crash reports before running tests.")
|
||||
|
||||
self.add_argument("tests",
|
||||
metavar="TEST_PATH",
|
||||
nargs="*",
|
||||
help="Path to test file, manifest file, or directory containing tests")
|
||||
|
||||
mozlog.commandline.add_logging_group(self)
|
||||
|
||||
def get_ip(self):
|
||||
import moznetwork
|
||||
if os.name != "nt":
|
||||
return moznetwork.get_ip()
|
||||
else:
|
||||
self.error(
|
||||
"ERROR: you must specify a --remote-webserver=<ip address>\n")
|
||||
|
||||
def set_default_suite(self, options):
|
||||
manifests = OrderedDict([("reftest.list", "reftest"),
|
||||
("crashtests.list", "crashtest"),
|
||||
("jstests.list", "jstestbrowser")])
|
||||
|
||||
for test_path in options.tests:
|
||||
file_name = os.path.basename(test_path)
|
||||
if file_name in manifests:
|
||||
options.suite = manifests[file_name]
|
||||
return
|
||||
|
||||
for test_path in options.tests:
|
||||
for manifest_file, suite in manifests.iteritems():
|
||||
if os.path.exists(os.path.join(test_path, manifest_file)):
|
||||
options.suite = suite
|
||||
return
|
||||
|
||||
self.error("Failed to determine test suite; supply --suite to set this explicitly")
|
||||
|
||||
def validate(self, options, reftest):
|
||||
if not options.tests:
|
||||
# Can't just set this in the argument parser because mach will set a default
|
||||
self.error("Must supply at least one path to a manifest file, test directory, or test file to run.")
|
||||
|
||||
if options.suite is None:
|
||||
self.set_default_suite(options)
|
||||
|
||||
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)
|
||||
|
||||
if options.reftestExtensionPath is None:
|
||||
if self.build_obj is not None:
|
||||
reftestExtensionPath = os.path.join(self.build_obj.topobjdir, "_tests",
|
||||
"reftest", "reftest")
|
||||
else:
|
||||
reftestExtensionPath = os.path.join(here, "reftest")
|
||||
options.reftestExtensionPath = os.path.normpath(reftestExtensionPath)
|
||||
|
||||
if (options.specialPowersExtensionPath is None and
|
||||
options.suite in ["crashtest", "jstestbrowser"]):
|
||||
if self.build_obj is not None:
|
||||
specialPowersExtensionPath = os.path.join(self.build_obj.topobjdir, "_tests",
|
||||
"reftest", "specialpowers")
|
||||
else:
|
||||
specialPowersExtensionPath = os.path.join(here, "specialpowers")
|
||||
options.specialPowersExtensionPath = os.path.normpath(specialPowersExtensionPath)
|
||||
|
||||
options.leakThresholds = {
|
||||
"default": options.defaultLeakThreshold,
|
||||
"tab": 5000, # See dependencies of bug 1051230.
|
||||
}
|
||||
|
||||
|
||||
class DesktopArgumentsParser(ReftestArgumentsParser):
|
||||
def __init__(self, **kwargs):
|
||||
super(DesktopArgumentsParser, self).__init__(**kwargs)
|
||||
|
||||
self.add_argument("--run-tests-in-parallel",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="runTestsInParallel",
|
||||
help="run tests in parallel if possible")
|
||||
|
||||
def _prefs_gpu(self):
|
||||
if mozinfo.os != "win":
|
||||
return ["layers.acceleration.force-enabled=true"]
|
||||
return []
|
||||
|
||||
def validate(self, options, reftest):
|
||||
super(DesktopArgumentsParser, self).validate(options, reftest)
|
||||
|
||||
if options.runTestsInParallel:
|
||||
if options.logFile is not None:
|
||||
self.error("cannot specify logfile with parallel tests")
|
||||
if options.totalChunks is not None or options.thisChunk is not None:
|
||||
self.error(
|
||||
"cannot specify thisChunk or totalChunks with parallel tests")
|
||||
if options.focusFilterMode != "all":
|
||||
self.error("cannot specify focusFilterMode with parallel tests")
|
||||
if options.debugger is not None:
|
||||
self.error("cannot specify a debugger with parallel tests")
|
||||
|
||||
if options.debugger:
|
||||
# valgrind and some debuggers may cause Gecko to start slowly. Make sure
|
||||
# marionette waits long enough to connect.
|
||||
options.marionette_port_timeout = 900
|
||||
options.marionette_socket_timeout = 540
|
||||
|
||||
if not options.tests:
|
||||
self.error("No test files specified.")
|
||||
|
||||
if options.app is None:
|
||||
bin_dir = (self.build_obj.get_binary_path() if
|
||||
self.build_obj and self.build_obj.substs[
|
||||
'MOZ_BUILD_APP'] != 'mobile/android'
|
||||
else None)
|
||||
|
||||
if bin_dir:
|
||||
options.app = bin_dir
|
||||
|
||||
if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2:
|
||||
options.symbolsPath = reftest.getFullPath(options.symbolsPath)
|
||||
|
||||
options.utilityPath = reftest.getFullPath(options.utilityPath)
|
||||
|
||||
|
||||
class B2GArgumentParser(ReftestArgumentsParser):
|
||||
def __init__(self, **kwargs):
|
||||
super(B2GArgumentParser, self).__init__(**kwargs)
|
||||
|
||||
self.add_argument("--browser-arg",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="browser_arg",
|
||||
help="Optional command-line arg to pass to the browser")
|
||||
|
||||
self.add_argument("--b2gpath",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="b2gPath",
|
||||
help="path to B2G repo or qemu dir")
|
||||
|
||||
self.add_argument("--emulator",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="emulator",
|
||||
help="Architecture of emulator to use: x86 or arm")
|
||||
|
||||
self.add_argument("--emulator-res",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="emulator_res",
|
||||
help="Emulator resolution of the format '<width>x<height>'")
|
||||
|
||||
self.add_argument("--no-window",
|
||||
action="store_true",
|
||||
dest="noWindow",
|
||||
default=False,
|
||||
help="Pass --no-window to the emulator")
|
||||
|
||||
self.add_argument("--adbpath",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="adb_path",
|
||||
default="adb",
|
||||
help="path to adb")
|
||||
|
||||
self.add_argument("--deviceIP",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="deviceIP",
|
||||
help="ip address of remote device to test")
|
||||
|
||||
self.add_argument("--devicePort",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="devicePort",
|
||||
default="20701",
|
||||
help="port of remote device to test")
|
||||
|
||||
self.add_argument("--remote-logfile",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="remoteLogFile",
|
||||
help="Name of log file on the device relative to the device root. PLEASE ONLY USE A FILENAME.")
|
||||
|
||||
self.add_argument("--remote-webserver",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="remoteWebServer",
|
||||
help="ip address where the remote web server is hosted at")
|
||||
|
||||
self.add_argument("--http-port",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="httpPort",
|
||||
help="ip address where the remote web server is hosted at")
|
||||
|
||||
self.add_argument("--ssl-port",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="sslPort",
|
||||
help="ip address where the remote web server is hosted at")
|
||||
|
||||
self.add_argument("--pidfile",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="pidFile",
|
||||
default="",
|
||||
help="name of the pidfile to generate")
|
||||
|
||||
self.add_argument("--gecko-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="geckoPath",
|
||||
help="the path to a gecko distribution that should "
|
||||
"be installed on the emulator prior to test")
|
||||
|
||||
self.add_argument("--logdir",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="logdir",
|
||||
help="directory to store log files")
|
||||
|
||||
self.add_argument('--busybox',
|
||||
action='store',
|
||||
type=str,
|
||||
dest='busybox',
|
||||
help="Path to busybox binary to install on device")
|
||||
|
||||
self.add_argument("--httpd-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="httpdPath",
|
||||
help="path to the httpd.js file")
|
||||
|
||||
self.add_argument("--profile",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="profile",
|
||||
help="for mulet testing, the path to the "
|
||||
"gaia profile to use")
|
||||
|
||||
self.add_argument("--mulet",
|
||||
action="store_true",
|
||||
dest="mulet",
|
||||
default=False,
|
||||
help="Run the tests on a B2G desktop build")
|
||||
|
||||
self.set_defaults(remoteTestRoot=None,
|
||||
logFile="reftest.log",
|
||||
autorun=True,
|
||||
closeWhenDone=True,
|
||||
testPath="")
|
||||
|
||||
def validate_remote(self, options, automation):
|
||||
if not options.app:
|
||||
options.app = automation.DEFAULT_APP
|
||||
|
||||
if not options.remoteTestRoot:
|
||||
options.remoteTestRoot = automation._devicemanager.deviceRoot + \
|
||||
"/reftest"
|
||||
|
||||
options.remoteProfile = options.remoteTestRoot + "/profile"
|
||||
|
||||
productRoot = options.remoteTestRoot + "/" + automation._product
|
||||
if options.utilityPath is None:
|
||||
options.utilityPath = productRoot + "/bin"
|
||||
|
||||
if not options.httpPort:
|
||||
options.httpPort = automation.DEFAULT_HTTP_PORT
|
||||
|
||||
if not options.sslPort:
|
||||
options.sslPort = automation.DEFAULT_SSL_PORT
|
||||
|
||||
if options.remoteWebServer is None:
|
||||
options.remoteWebServer = self.get_ip()
|
||||
|
||||
options.webServer = options.remoteWebServer
|
||||
|
||||
if options.geckoPath and not options.emulator:
|
||||
self.error(
|
||||
"You must specify --emulator if you specify --gecko-path")
|
||||
|
||||
if options.logdir and not options.emulator:
|
||||
self.error("You must specify --emulator if you specify --logdir")
|
||||
|
||||
if options.remoteLogFile is 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 is 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 RemoteArgumentsParser(ReftestArgumentsParser):
|
||||
def __init__(self, **kwargs):
|
||||
super(RemoteArgumentsParser, self).__init__()
|
||||
|
||||
# app, xrePath and utilityPath variables are set in main function
|
||||
self.set_defaults(logFile="reftest.log",
|
||||
app="",
|
||||
xrePath="",
|
||||
utilityPath="",
|
||||
localLogName=None)
|
||||
|
||||
self.add_argument("--remote-app-path",
|
||||
action="store",
|
||||
type=str,
|
||||
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.")
|
||||
|
||||
self.add_argument("--adbpath",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="adb_path",
|
||||
default="adb",
|
||||
help="path to adb")
|
||||
|
||||
self.add_argument("--deviceIP",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="deviceIP",
|
||||
help="ip address of remote device to test")
|
||||
|
||||
self.add_argument("--deviceSerial",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="deviceSerial",
|
||||
help="adb serial number of remote device to test")
|
||||
|
||||
self.add_argument("--devicePort",
|
||||
action="store",
|
||||
type=str,
|
||||
default="20701",
|
||||
dest="devicePort",
|
||||
help="port of remote device to test")
|
||||
|
||||
self.add_argument("--remote-product-name",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="remoteProductName",
|
||||
default="fennec",
|
||||
help="Name of product to test - either fennec or firefox, defaults to fennec")
|
||||
|
||||
self.add_argument("--remote-webserver",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="remoteWebServer",
|
||||
help="IP Address of the webserver hosting the reftest content")
|
||||
|
||||
self.add_argument("--http-port",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="httpPort",
|
||||
help="port of the web server for http traffic")
|
||||
|
||||
self.add_argument("--ssl-port",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="sslPort",
|
||||
help="Port for https traffic to the web server")
|
||||
|
||||
self.add_argument("--remote-logfile",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="remoteLogFile",
|
||||
default="reftest.log",
|
||||
help="Name of log file on the device relative to device root. PLEASE USE ONLY A FILENAME.")
|
||||
|
||||
self.add_argument("--pidfile",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="pidFile",
|
||||
default="",
|
||||
help="name of the pidfile to generate")
|
||||
|
||||
self.add_argument("--dm_trans",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="dm_trans",
|
||||
default="sut",
|
||||
help="the transport to use to communicate with device: [adb|sut]; default=sut")
|
||||
|
||||
self.add_argument("--remoteTestRoot",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="remoteTestRoot",
|
||||
help="remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
|
||||
|
||||
self.add_argument("--httpd-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="httpdPath",
|
||||
help="path to the httpd.js file")
|
||||
|
||||
self.add_argument("--no-device-info",
|
||||
action="store_false",
|
||||
dest="printDeviceInfo",
|
||||
default=True,
|
||||
help="do not display verbose diagnostics about the remote device")
|
||||
|
||||
def validate_remote(self, options, automation):
|
||||
# Ensure our defaults are set properly for everything we can infer
|
||||
if not options.remoteTestRoot:
|
||||
options.remoteTestRoot = automation._devicemanager.deviceRoot + \
|
||||
'/reftest'
|
||||
options.remoteProfile = options.remoteTestRoot + "/profile"
|
||||
|
||||
if options.remoteWebServer is None:
|
||||
options.remoteWebServer = self.get_ip()
|
||||
|
||||
# Verify that our remotewebserver is set properly
|
||||
if options.remoteWebServer == '127.0.0.1':
|
||||
self.error("ERROR: Either you specified the loopback for the remote webserver or ",
|
||||
"your local IP cannot be detected. Please provide the local ip in --remote-webserver")
|
||||
|
||||
if not options.httpPort:
|
||||
options.httpPort = automation.DEFAULT_HTTP_PORT
|
||||
|
||||
if not options.sslPort:
|
||||
options.sslPort = automation.DEFAULT_SSL_PORT
|
||||
|
||||
# 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:
|
||||
self.error(
|
||||
"ERROR: You cannot specify both the remoteAppPath and the app")
|
||||
elif options.remoteAppPath:
|
||||
options.app = options.remoteTestRoot + "/" + options.remoteAppPath
|
||||
elif options.app is None:
|
||||
# Neither remoteAppPath nor app are set -- error
|
||||
self.error("ERROR: You must specify either appPath or app")
|
||||
|
||||
if options.xrePath is None:
|
||||
self.error(
|
||||
"ERROR: You must specify the path to the controller xre directory")
|
||||
else:
|
||||
# Ensure xrepath is a full path
|
||||
options.xrePath = os.path.abspath(options.xrePath)
|
||||
|
||||
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 != "":
|
||||
with open(options.pidFile, 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
# 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")
|
||||
|
||||
if not options.ignoreWindowSize:
|
||||
parts = automation._devicemanager.getInfo(
|
||||
'screen')['screen'][0].split()
|
||||
width = int(parts[0].split(':')[1])
|
||||
height = int(parts[1].split(':')[1])
|
||||
if (width < 1366 or height < 1050):
|
||||
self.error("ERROR: Invalid screen resolution %sx%s, please adjust to 1366x1050 or higher" % (
|
||||
width, height))
|
||||
|
||||
# Disable e10s by default on Android because we don't run Android
|
||||
# e10s jobs anywhere yet.
|
||||
options.e10s = False
|
||||
return options
|
||||
412
layout/tools/reftest/remotereftest.py
Normal file
412
layout/tools/reftest/remotereftest.py
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
# 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
|
||||
import urllib2
|
||||
|
||||
import mozdevice
|
||||
import mozinfo
|
||||
from automation import Automation
|
||||
from remoteautomation import RemoteAutomation, fennecLogcatFilters
|
||||
|
||||
from output import OutputHandler
|
||||
from runreftest import RefTest, ReftestResolver
|
||||
import reftestcommandline
|
||||
|
||||
# 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(__file__)))
|
||||
|
||||
|
||||
class RemoteReftestResolver(ReftestResolver):
|
||||
def absManifestPath(self, path):
|
||||
script_abs_path = os.path.join(SCRIPT_DIRECTORY, path)
|
||||
if os.path.exists(script_abs_path):
|
||||
rv = script_abs_path
|
||||
elif os.path.exists(os.path.abspath(path)):
|
||||
rv = os.path.abspath(path)
|
||||
else:
|
||||
print >> sys.stderr, "Could not find manifest %s" % script_abs_path
|
||||
sys.exit(1)
|
||||
return os.path.normpath(rv)
|
||||
|
||||
def manifestURL(self, options, path):
|
||||
# Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside' the webroot
|
||||
# It's possible for this url to have a leading "..", but reftest.js will fix that up
|
||||
relPath = os.path.relpath(path, SCRIPT_DIRECTORY)
|
||||
return "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, relPath)
|
||||
|
||||
|
||||
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):
|
||||
use_marionette = False
|
||||
remoteApp = ''
|
||||
resolver_cls = RemoteReftestResolver
|
||||
|
||||
def __init__(self, automation, devicemanager, options, scriptDir):
|
||||
RefTest.__init__(self)
|
||||
self.automation = 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()
|
||||
self.automation.deleteTombstones()
|
||||
|
||||
self._populate_logger(options)
|
||||
outputHandler = OutputHandler(self.log, options.utilityPath, options.symbolsPath)
|
||||
# RemoteAutomation.py's 'messageLogger' is also used by mochitest. Mimic a mochitest
|
||||
# MessageLogger object to re-use this code path.
|
||||
outputHandler.write = outputHandler.__call__
|
||||
self.automation._processArgs['messageLogger'] = outputHandler
|
||||
|
||||
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, manifest):
|
||||
profile = RefTest.createReftestProfile(self,
|
||||
options,
|
||||
manifest,
|
||||
server=options.remoteWebServer,
|
||||
port=options.httpPort)
|
||||
profileDir = profile.profile
|
||||
|
||||
prefs = {}
|
||||
prefs["app.update.url.android"] = ""
|
||||
prefs["browser.firstrun.show.localepicker"] = False
|
||||
prefs["reftest.remote"] = True
|
||||
prefs["datareporting.policy.dataSubmissionPolicyBypassAcceptance"] = True
|
||||
|
||||
prefs["layout.css.devPixelsPerPx"] = "1.0"
|
||||
# Because Fennec is a little wacky (see bug 1156817) we need to load the
|
||||
# reftest pages at 1.0 zoom, rather than zooming to fit the CSS viewport.
|
||||
prefs["apz.allow_zooming"] = False
|
||||
|
||||
# Set the extra prefs.
|
||||
profile.set_preferences(prefs)
|
||||
|
||||
try:
|
||||
self._devicemanager.pushDir(profileDir, options.remoteProfile)
|
||||
self._devicemanager.chmodDir(options.remoteProfile)
|
||||
except mozdevice.DMError:
|
||||
print "Automation Error: Failed to copy profiledir to device"
|
||||
raise
|
||||
|
||||
return profile
|
||||
|
||||
def copyExtraFilesToProfile(self, options, profile):
|
||||
profileDir = profile.profile
|
||||
RefTest.copyExtraFilesToProfile(self, options, profile)
|
||||
try:
|
||||
self._devicemanager.pushDir(profileDir, options.remoteProfile)
|
||||
self._devicemanager.chmodDir(options.remoteProfile)
|
||||
except mozdevice.DMError:
|
||||
print "Automation Error: Failed to copy extra files to device"
|
||||
raise
|
||||
|
||||
def printDeviceInfo(self, printLogcat=False):
|
||||
try:
|
||||
if printLogcat:
|
||||
logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
|
||||
print ''.join(logcat)
|
||||
print "Device info:"
|
||||
devinfo = self._devicemanager.getInfo()
|
||||
for category in devinfo:
|
||||
if type(devinfo[category]) is list:
|
||||
print " %s:" % category
|
||||
for item in devinfo[category]:
|
||||
print " %s" % item
|
||||
else:
|
||||
print " %s: %s" % (category, devinfo[category])
|
||||
print "Test root: %s" % self._devicemanager.deviceRoot
|
||||
except mozdevice.DMError:
|
||||
print "WARNING: Error getting device information"
|
||||
|
||||
def environment(self, **kwargs):
|
||||
return self.automation.environment(**kwargs)
|
||||
|
||||
def buildBrowserEnv(self, options, profileDir):
|
||||
browserEnv = RefTest.buildBrowserEnv(self, options, profileDir)
|
||||
# remove desktop environment not used on device
|
||||
if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
|
||||
del browserEnv["XPCOM_MEM_BLOAT_LOG"]
|
||||
return browserEnv
|
||||
|
||||
def runApp(self, profile, binary, cmdargs, env,
|
||||
timeout=None, debuggerInfo=None,
|
||||
symbolsPath=None, options=None,
|
||||
valgrindPath=None, valgrindArgs=None, valgrindSuppFiles=None):
|
||||
status = self.automation.runApp(None, env,
|
||||
binary,
|
||||
profile.profile,
|
||||
cmdargs,
|
||||
utilityPath=options.utilityPath,
|
||||
xrePath=options.xrePath,
|
||||
debuggerInfo=debuggerInfo,
|
||||
symbolsPath=symbolsPath,
|
||||
timeout=timeout)
|
||||
return status
|
||||
|
||||
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 run_test_harness(parser, options):
|
||||
if options.dm_trans == 'sut' and options.deviceIP == None:
|
||||
print "Error: If --dm_trans = sut, you must provide a device IP to connect to via the --deviceIP option"
|
||||
return 1
|
||||
|
||||
dm_args = {
|
||||
'deviceRoot': options.remoteTestRoot,
|
||||
'host': options.deviceIP,
|
||||
'port': options.devicePort,
|
||||
}
|
||||
|
||||
dm_cls = mozdevice.DroidSUT
|
||||
if options.dm_trans == 'adb':
|
||||
dm_args['adbPath'] = options.adb_path
|
||||
if not dm_args['host']:
|
||||
dm_args['deviceSerial'] = options.deviceSerial
|
||||
dm_cls = mozdevice.DroidADB
|
||||
|
||||
try:
|
||||
dm = dm_cls(**dm_args)
|
||||
except mozdevice.DMError:
|
||||
traceback.print_exc()
|
||||
print "Automation Error: exception while initializing devicemanager. Most likely the device is not in a testable state."
|
||||
return 1
|
||||
|
||||
automation = RemoteAutomation(None)
|
||||
automation.setDeviceManager(dm)
|
||||
|
||||
if options.remoteProductName:
|
||||
automation.setProduct(options.remoteProductName)
|
||||
|
||||
# Set up the defaults and ensure options are set
|
||||
parser.validate_remote(options, automation)
|
||||
|
||||
# Check that Firefox is installed
|
||||
expected = options.app.split('/')[-1]
|
||||
installed = dm.shellCheckOutput(['pm', 'list', 'packages', expected])
|
||||
if expected not in installed:
|
||||
print "%s is not installed on this device" % expected
|
||||
return 1
|
||||
|
||||
automation.setAppName(options.app)
|
||||
automation.setRemoteProfile(options.remoteProfile)
|
||||
automation.setRemoteLog(options.remoteLogFile)
|
||||
reftest = RemoteReftest(automation, dm, options, SCRIPT_DIRECTORY)
|
||||
parser.validate(options, reftest)
|
||||
|
||||
if mozinfo.info['debug']:
|
||||
print "changing timeout for remote debug reftests from %s to 600 seconds" % options.timeout
|
||||
options.timeout = 600
|
||||
|
||||
# Hack in a symbolic link for jsreftest
|
||||
os.system("ln -s ../jsreftest " + str(os.path.join(SCRIPT_DIRECTORY, "jsreftest")))
|
||||
|
||||
# Start the webserver
|
||||
retVal = reftest.startWebServer(options)
|
||||
if retVal:
|
||||
return retVal
|
||||
|
||||
procName = options.app.split('/')[-1]
|
||||
if (dm.processExist(procName)):
|
||||
dm.killProcess(procName)
|
||||
|
||||
if options.printDeviceInfo:
|
||||
reftest.printDeviceInfo()
|
||||
|
||||
#an example manifest name to use on the cli
|
||||
# manifest = "http://" + options.remoteWebServer + "/reftests/layout/reftests/reftest-sanity/reftest.list"
|
||||
retVal = 0
|
||||
try:
|
||||
dm.recordLogcat()
|
||||
retVal = reftest.runTests(options.tests, options)
|
||||
except:
|
||||
print "Automation Error: Exception caught while running tests"
|
||||
traceback.print_exc()
|
||||
retVal = 1
|
||||
|
||||
reftest.stopWebServer(options)
|
||||
|
||||
if options.printDeviceInfo:
|
||||
reftest.printDeviceInfo(printLogcat=True)
|
||||
|
||||
return retVal
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = reftestcommandline.RemoteArgumentsParser()
|
||||
options = parser.parse_args()
|
||||
sys.exit(run_test_harness(parser, options))
|
||||
747
layout/tools/reftest/runreftest.py
Normal file
747
layout/tools/reftest/runreftest.py
Normal file
|
|
@ -0,0 +1,747 @@
|
|||
# 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 collections
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
SCRIPT_DIRECTORY = os.path.abspath(
|
||||
os.path.realpath(os.path.dirname(__file__)))
|
||||
if SCRIPT_DIRECTORY not in sys.path:
|
||||
sys.path.insert(0, SCRIPT_DIRECTORY)
|
||||
|
||||
import mozcrash
|
||||
import mozdebug
|
||||
import mozinfo
|
||||
import mozleak
|
||||
import mozlog
|
||||
import mozprocess
|
||||
import mozprofile
|
||||
import mozrunner
|
||||
from mozrunner.utils import get_stack_fixer_function, test_environment
|
||||
from mozscreenshot import printstatus, dump_screen
|
||||
|
||||
try:
|
||||
from marionette_driver.addons import Addons
|
||||
from marionette_harness import Marionette
|
||||
except ImportError, e:
|
||||
# Defer ImportError until attempt to use Marionette
|
||||
def reraise(*args, **kwargs):
|
||||
raise(e)
|
||||
Marionette = reraise
|
||||
|
||||
from output import OutputHandler, ReftestFormatter
|
||||
import reftestcommandline
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
try:
|
||||
from mozbuild.base import MozbuildObject
|
||||
build_obj = MozbuildObject.from_environment(cwd=here)
|
||||
except ImportError:
|
||||
build_obj = None
|
||||
|
||||
|
||||
def categoriesToRegex(categoryList):
|
||||
return "\\(" + ', '.join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)"
|
||||
summaryLines = [('Successful', [('pass', 'pass'), ('loadOnly', 'load only')]),
|
||||
('Unexpected', [('fail', 'unexpected fail'),
|
||||
('pass', 'unexpected pass'),
|
||||
('asserts', 'unexpected asserts'),
|
||||
('fixedAsserts', 'unexpected fixed asserts'),
|
||||
('failedLoad', 'failed load'),
|
||||
('exception', 'exception')]),
|
||||
('Known problems', [('knownFail', 'known fail'),
|
||||
('knownAsserts', 'known asserts'),
|
||||
('random', 'random'),
|
||||
('skipped', 'skipped'),
|
||||
('slow', 'slow')])]
|
||||
|
||||
# Python's print is not threadsafe.
|
||||
printLock = threading.Lock()
|
||||
|
||||
|
||||
class ReftestThread(threading.Thread):
|
||||
def __init__(self, cmdargs):
|
||||
threading.Thread.__init__(self)
|
||||
self.cmdargs = cmdargs
|
||||
self.summaryMatches = {}
|
||||
self.retcode = -1
|
||||
for text, _ in summaryLines:
|
||||
self.summaryMatches[text] = None
|
||||
|
||||
def run(self):
|
||||
with printLock:
|
||||
print "Starting thread with", self.cmdargs
|
||||
sys.stdout.flush()
|
||||
process = subprocess.Popen(self.cmdargs, stdout=subprocess.PIPE)
|
||||
for chunk in self.chunkForMergedOutput(process.stdout):
|
||||
with printLock:
|
||||
print chunk,
|
||||
sys.stdout.flush()
|
||||
self.retcode = process.wait()
|
||||
|
||||
def chunkForMergedOutput(self, logsource):
|
||||
"""Gather lines together that should be printed as one atomic unit.
|
||||
Individual test results--anything between 'REFTEST TEST-START' and
|
||||
'REFTEST TEST-END' lines--are an atomic unit. Lines with data from
|
||||
summaries are parsed and the data stored for later aggregation.
|
||||
Other lines are considered their own atomic units and are permitted
|
||||
to intermix freely."""
|
||||
testStartRegex = re.compile("^REFTEST TEST-START")
|
||||
testEndRegex = re.compile("^REFTEST TEST-END")
|
||||
summaryHeadRegex = re.compile("^REFTEST INFO \\| Result summary:")
|
||||
summaryRegexFormatString = "^REFTEST INFO \\| (?P<message>{text}): (?P<total>\\d+) {regex}"
|
||||
summaryRegexStrings = [summaryRegexFormatString.format(text=text,
|
||||
regex=categoriesToRegex(categories))
|
||||
for (text, categories) in summaryLines]
|
||||
summaryRegexes = [re.compile(regex) for regex in summaryRegexStrings]
|
||||
|
||||
for line in logsource:
|
||||
if testStartRegex.search(line) is not None:
|
||||
chunkedLines = [line]
|
||||
for lineToBeChunked in logsource:
|
||||
chunkedLines.append(lineToBeChunked)
|
||||
if testEndRegex.search(lineToBeChunked) is not None:
|
||||
break
|
||||
yield ''.join(chunkedLines)
|
||||
continue
|
||||
|
||||
haveSuppressedSummaryLine = False
|
||||
for regex in summaryRegexes:
|
||||
match = regex.search(line)
|
||||
if match is not None:
|
||||
self.summaryMatches[match.group('message')] = match
|
||||
haveSuppressedSummaryLine = True
|
||||
break
|
||||
if haveSuppressedSummaryLine:
|
||||
continue
|
||||
|
||||
if summaryHeadRegex.search(line) is None:
|
||||
yield line
|
||||
|
||||
class ReftestResolver(object):
|
||||
def defaultManifest(self, suite):
|
||||
return {"reftest": "reftest.list",
|
||||
"crashtest": "crashtests.list",
|
||||
"jstestbrowser": "jstests.list"}[suite]
|
||||
|
||||
def directoryManifest(self, suite, path):
|
||||
return os.path.join(path, self.defaultManifest(suite))
|
||||
|
||||
def findManifest(self, suite, test_file, subdirs=True):
|
||||
"""Return a tuple of (manifest-path, filter-string) for running test_file.
|
||||
|
||||
test_file is a path to a test or a manifest file
|
||||
"""
|
||||
rv = []
|
||||
default_manifest = self.defaultManifest(suite)
|
||||
if not os.path.isabs(test_file):
|
||||
test_file = self.absManifestPath(test_file)
|
||||
|
||||
if os.path.isdir(test_file):
|
||||
for dirpath, dirnames, filenames in os.walk(test_file):
|
||||
if default_manifest in filenames:
|
||||
rv.append((os.path.join(dirpath, default_manifest), None))
|
||||
# We keep recursing into subdirectories which means that in the case
|
||||
# of include directives we get the same manifest multiple times.
|
||||
# However reftest.js will only read each manifest once
|
||||
|
||||
elif test_file.endswith('.list'):
|
||||
if os.path.exists(test_file):
|
||||
rv = [(test_file, None)]
|
||||
else:
|
||||
dirname, pathname = os.path.split(test_file)
|
||||
found = True
|
||||
while not os.path.exists(os.path.join(dirname, default_manifest)):
|
||||
dirname, suffix = os.path.split(dirname)
|
||||
pathname = os.path.join(suffix, pathname)
|
||||
if os.path.dirname(dirname) == dirname:
|
||||
found = False
|
||||
break
|
||||
if found:
|
||||
rv = [(os.path.join(dirname, default_manifest),
|
||||
r".*(?:/|\\)%s(?:[#?].*)?$" % pathname)]
|
||||
|
||||
return rv
|
||||
|
||||
def absManifestPath(self, path):
|
||||
return os.path.normpath(os.path.abspath(path))
|
||||
|
||||
def manifestURL(self, options, path):
|
||||
return "file://%s" % path
|
||||
|
||||
def resolveManifests(self, options, tests):
|
||||
suite = options.suite
|
||||
manifests = {}
|
||||
for testPath in tests:
|
||||
for manifest, filter_str in self.findManifest(suite, testPath):
|
||||
manifest = self.manifestURL(options, manifest)
|
||||
if manifest not in manifests:
|
||||
manifests[manifest] = set()
|
||||
manifests[manifest].add(filter_str)
|
||||
|
||||
for key in manifests.iterkeys():
|
||||
if None in manifests[key]:
|
||||
manifests[key] = None
|
||||
else:
|
||||
manifests[key] = "|".join(list(manifests[key]))
|
||||
return manifests
|
||||
|
||||
|
||||
class RefTest(object):
|
||||
use_marionette = True
|
||||
oldcwd = os.getcwd()
|
||||
resolver_cls = ReftestResolver
|
||||
|
||||
def __init__(self):
|
||||
self.update_mozinfo()
|
||||
self.lastTestSeen = 'reftest'
|
||||
self.haveDumpedScreen = False
|
||||
self.resolver = self.resolver_cls()
|
||||
self.log = None
|
||||
|
||||
def _populate_logger(self, options):
|
||||
if self.log:
|
||||
return
|
||||
|
||||
mozlog.commandline.log_formatters["tbpl"] = (ReftestFormatter,
|
||||
"Reftest specific formatter for the"
|
||||
"benefit of legacy log parsers and"
|
||||
"tools such as the reftest analyzer")
|
||||
fmt_options = {}
|
||||
if not options.log_tbpl_level and os.environ.get('MOZ_REFTEST_VERBOSE'):
|
||||
options.log_tbpl_level = fmt_options['level'] = 'debug'
|
||||
self.log = mozlog.commandline.setup_logging(
|
||||
"reftest harness", options, {"tbpl": sys.stdout}, fmt_options)
|
||||
|
||||
def update_mozinfo(self):
|
||||
"""walk up directories to find mozinfo.json update the info"""
|
||||
# TODO: This should go in a more generic place, e.g. mozinfo
|
||||
|
||||
path = SCRIPT_DIRECTORY
|
||||
dirs = set()
|
||||
while path != os.path.expanduser('~'):
|
||||
if path in dirs:
|
||||
break
|
||||
dirs.add(path)
|
||||
path = os.path.split(path)[0]
|
||||
mozinfo.find_and_update_from_json(*dirs)
|
||||
|
||||
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 createReftestProfile(self, options, manifests, server='localhost', port=0,
|
||||
profile_to_clone=None):
|
||||
"""Sets up a profile for reftest.
|
||||
|
||||
:param options: Object containing command line options
|
||||
:param manifests: Dictionary of the form {manifest_path: [filters]}
|
||||
:param server: Server name to use for http tests
|
||||
:param profile_to_clone: Path to a profile to use as the basis for the
|
||||
test profile
|
||||
"""
|
||||
|
||||
locations = mozprofile.permissions.ServerLocations()
|
||||
locations.add_host(server, scheme='http', port=port)
|
||||
locations.add_host(server, scheme='https', port=port)
|
||||
|
||||
# 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-preferences.js .
|
||||
prefs = {}
|
||||
prefs['reftest.timeout'] = options.timeout * 1000
|
||||
if options.totalChunks:
|
||||
prefs['reftest.totalChunks'] = options.totalChunks
|
||||
if options.thisChunk:
|
||||
prefs['reftest.thisChunk'] = options.thisChunk
|
||||
if options.logFile:
|
||||
prefs['reftest.logFile'] = options.logFile
|
||||
if options.ignoreWindowSize:
|
||||
prefs['reftest.ignoreWindowSize'] = True
|
||||
if options.shuffle:
|
||||
prefs['reftest.shuffle'] = True
|
||||
if options.repeat:
|
||||
prefs['reftest.repeat'] = options.repeat
|
||||
if options.runUntilFailure:
|
||||
prefs['reftest.runUntilFailure'] = True
|
||||
prefs['reftest.focusFilterMode'] = options.focusFilterMode
|
||||
prefs['reftest.logLevel'] = options.log_tbpl_level or 'info'
|
||||
prefs['reftest.manifests'] = json.dumps(manifests)
|
||||
|
||||
if options.e10s:
|
||||
prefs['browser.tabs.remote.autostart'] = True
|
||||
prefs['extensions.e10sBlocksEnabling'] = False
|
||||
|
||||
# Bug 1262954: For winXP + e10s disable acceleration
|
||||
if platform.system() in ("Windows", "Microsoft") and \
|
||||
'5.1' in platform.version() and options.e10s:
|
||||
prefs['layers.acceleration.disabled'] = True
|
||||
|
||||
# Bug 1300355: Disable canvas cache for win7 as it uses
|
||||
# too much memory and causes OOMs.
|
||||
if platform.system() in ("Windows", "Microsoft") and \
|
||||
'6.1' in platform.version():
|
||||
prefs['reftest.nocache'] = True
|
||||
|
||||
if options.marionette:
|
||||
port = options.marionette.split(':')[1]
|
||||
prefs['marionette.defaultPrefs.port'] = int(port)
|
||||
|
||||
preference_file = os.path.join(here, 'reftest-preferences.js')
|
||||
prefs.update(mozprofile.Preferences.read_prefs(preference_file))
|
||||
|
||||
for v in options.extraPrefs:
|
||||
thispref = v.split('=')
|
||||
if len(thispref) < 2:
|
||||
print "Error: syntax error in --setpref=" + v
|
||||
sys.exit(1)
|
||||
prefs[thispref[0]] = thispref[1].strip()
|
||||
|
||||
addons = []
|
||||
if not self.use_marionette:
|
||||
addons.append(options.reftestExtensionPath)
|
||||
|
||||
if options.specialPowersExtensionPath is not None:
|
||||
if not self.use_marionette:
|
||||
addons.append(options.specialPowersExtensionPath)
|
||||
# SpecialPowers requires insecure automation-only features that we
|
||||
# put behind a pref.
|
||||
prefs['security.turn_off_all_security_so_that_viruses_can_take_over_this_computer'] = True
|
||||
|
||||
for pref in prefs:
|
||||
prefs[pref] = mozprofile.Preferences.cast(prefs[pref])
|
||||
|
||||
# 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):
|
||||
addons.append(os.path.join(distExtDir, f))
|
||||
|
||||
# Install custom extensions.
|
||||
for f in options.extensionsToInstall:
|
||||
addons.append(self.getFullPath(f))
|
||||
|
||||
kwargs = {'addons': addons,
|
||||
'preferences': prefs,
|
||||
'locations': locations}
|
||||
if profile_to_clone:
|
||||
profile = mozprofile.Profile.clone(profile_to_clone, **kwargs)
|
||||
else:
|
||||
profile = mozprofile.Profile(**kwargs)
|
||||
|
||||
self.copyExtraFilesToProfile(options, profile)
|
||||
return profile
|
||||
|
||||
def environment(self, **kwargs):
|
||||
kwargs['log'] = self.log
|
||||
return test_environment(**kwargs)
|
||||
|
||||
def buildBrowserEnv(self, options, profileDir):
|
||||
browserEnv = self.environment(
|
||||
xrePath=options.xrePath, debugger=options.debugger)
|
||||
browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
|
||||
|
||||
if mozinfo.info["asan"]:
|
||||
# Disable leak checking for reftests for now
|
||||
if "ASAN_OPTIONS" in browserEnv:
|
||||
browserEnv["ASAN_OPTIONS"] += ":detect_leaks=0"
|
||||
else:
|
||||
browserEnv["ASAN_OPTIONS"] = "detect_leaks=0"
|
||||
|
||||
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 killNamedOrphans(self, pname):
|
||||
""" Kill orphan processes matching the given command name """
|
||||
self.log.info("Checking for orphan %s processes..." % pname)
|
||||
|
||||
def _psInfo(line):
|
||||
if pname in line:
|
||||
self.log.info(line)
|
||||
process = mozprocess.ProcessHandler(['ps', '-f'],
|
||||
processOutputLine=_psInfo)
|
||||
process.run()
|
||||
process.wait()
|
||||
|
||||
def _psKill(line):
|
||||
parts = line.split()
|
||||
if len(parts) == 3 and parts[0].isdigit():
|
||||
pid = int(parts[0])
|
||||
if parts[2] == pname and parts[1] == '1':
|
||||
self.log.info("killing %s orphan with pid %d" % (pname, pid))
|
||||
try:
|
||||
os.kill(
|
||||
pid, getattr(signal, "SIGKILL", signal.SIGTERM))
|
||||
except Exception as e:
|
||||
self.log.info("Failed to kill process %d: %s" %
|
||||
(pid, str(e)))
|
||||
process = mozprocess.ProcessHandler(['ps', '-o', 'pid,ppid,comm'],
|
||||
processOutputLine=_psKill)
|
||||
process.run()
|
||||
process.wait()
|
||||
|
||||
def cleanup(self, profileDir):
|
||||
if profileDir:
|
||||
shutil.rmtree(profileDir, True)
|
||||
|
||||
def runTests(self, tests, options, cmdargs=None):
|
||||
cmdargs = cmdargs or []
|
||||
self._populate_logger(options)
|
||||
|
||||
# Despite our efforts to clean up servers started by this script, in practice
|
||||
# we still see infrequent cases where a process is orphaned and interferes
|
||||
# with future tests, typically because the old server is keeping the port in use.
|
||||
# Try to avoid those failures by checking for and killing orphan servers before
|
||||
# trying to start new ones.
|
||||
self.killNamedOrphans('ssltunnel')
|
||||
self.killNamedOrphans('xpcshell')
|
||||
|
||||
if options.cleanupCrashes:
|
||||
mozcrash.cleanup_pending_crash_reports()
|
||||
|
||||
manifests = self.resolver.resolveManifests(options, tests)
|
||||
if options.filter:
|
||||
manifests[""] = options.filter
|
||||
|
||||
if not getattr(options, 'runTestsInParallel', False):
|
||||
return self.runSerialTests(manifests, options, cmdargs)
|
||||
|
||||
cpuCount = multiprocessing.cpu_count()
|
||||
|
||||
# We have the directive, technology, and machine to run multiple test instances.
|
||||
# Experimentation says that reftests are not overly CPU-intensive, so we can run
|
||||
# multiple jobs per CPU core.
|
||||
#
|
||||
# Our Windows machines in automation seem to get upset when we run a lot of
|
||||
# simultaneous tests on them, so tone things down there.
|
||||
if sys.platform == 'win32':
|
||||
jobsWithoutFocus = cpuCount
|
||||
else:
|
||||
jobsWithoutFocus = 2 * cpuCount
|
||||
|
||||
totalJobs = jobsWithoutFocus + 1
|
||||
perProcessArgs = [sys.argv[:] for i in range(0, totalJobs)]
|
||||
|
||||
host = 'localhost'
|
||||
port = 2828
|
||||
if options.marionette:
|
||||
host, port = options.marionette.split(':')
|
||||
|
||||
# First job is only needs-focus tests. Remaining jobs are
|
||||
# non-needs-focus and chunked.
|
||||
perProcessArgs[0].insert(-1, "--focus-filter-mode=needs-focus")
|
||||
for (chunkNumber, jobArgs) in enumerate(perProcessArgs[1:], start=1):
|
||||
jobArgs[-1:-1] = ["--focus-filter-mode=non-needs-focus",
|
||||
"--total-chunks=%d" % jobsWithoutFocus,
|
||||
"--this-chunk=%d" % chunkNumber,
|
||||
"--marionette=%s:%d" % (host, port)]
|
||||
port += 1
|
||||
|
||||
for jobArgs in perProcessArgs:
|
||||
try:
|
||||
jobArgs.remove("--run-tests-in-parallel")
|
||||
except:
|
||||
pass
|
||||
jobArgs[0:0] = [sys.executable, "-u"]
|
||||
|
||||
threads = [ReftestThread(args) for args in perProcessArgs[1:]]
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
while True:
|
||||
# The test harness in each individual thread will be doing timeout
|
||||
# handling on its own, so we shouldn't need to worry about any of
|
||||
# the threads hanging for arbitrarily long.
|
||||
for t in threads:
|
||||
t.join(10)
|
||||
if not any(t.is_alive() for t in threads):
|
||||
break
|
||||
|
||||
# Run the needs-focus tests serially after the other ones, so we don't
|
||||
# have to worry about races between the needs-focus tests *actually*
|
||||
# needing focus and the dummy windows in the non-needs-focus tests
|
||||
# trying to focus themselves.
|
||||
focusThread = ReftestThread(perProcessArgs[0])
|
||||
focusThread.start()
|
||||
focusThread.join()
|
||||
|
||||
# Output the summaries that the ReftestThread filters suppressed.
|
||||
summaryObjects = [collections.defaultdict(int) for s in summaryLines]
|
||||
for t in threads:
|
||||
for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines):
|
||||
threadMatches = t.summaryMatches[text]
|
||||
for (attribute, description) in categories:
|
||||
amount = int(
|
||||
threadMatches.group(attribute) if threadMatches else 0)
|
||||
summaryObj[attribute] += amount
|
||||
amount = int(
|
||||
threadMatches.group('total') if threadMatches else 0)
|
||||
summaryObj['total'] += amount
|
||||
|
||||
print 'REFTEST INFO | Result summary:'
|
||||
for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines):
|
||||
details = ', '.join(["%d %s" % (summaryObj[attribute], description) for (
|
||||
attribute, description) in categories])
|
||||
print 'REFTEST INFO | ' + text + ': ' + str(summaryObj['total']) + ' (' + details + ')'
|
||||
|
||||
return int(any(t.retcode != 0 for t in threads))
|
||||
|
||||
def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo):
|
||||
"""handle process output timeout"""
|
||||
# TODO: bug 913975 : _processOutput should call self.processOutputLine
|
||||
# one more time one timeout (I think)
|
||||
self.log.error("%s | application timed out after %d seconds with no output" % (self.lastTestSeen, int(timeout)))
|
||||
self.log.error("Force-terminating active process(es).");
|
||||
self.killAndGetStack(
|
||||
proc, utilityPath, debuggerInfo, dump_screen=not debuggerInfo)
|
||||
|
||||
def dumpScreen(self, utilityPath):
|
||||
if self.haveDumpedScreen:
|
||||
self.log.info("Not taking screenshot here: see the one that was previously logged")
|
||||
return
|
||||
self.haveDumpedScreen = True
|
||||
dump_screen(utilityPath, self.log)
|
||||
|
||||
def killAndGetStack(self, process, utilityPath, debuggerInfo, dump_screen=False):
|
||||
"""
|
||||
Kill the process, preferrably in a way that gets us a stack trace.
|
||||
Also attempts to obtain a screenshot before killing the process
|
||||
if specified.
|
||||
"""
|
||||
|
||||
if dump_screen:
|
||||
self.dumpScreen(utilityPath)
|
||||
|
||||
if mozinfo.info.get('crashreporter', True) and not debuggerInfo:
|
||||
if mozinfo.isWin:
|
||||
# We should have a "crashinject" program in our utility path
|
||||
crashinject = os.path.normpath(
|
||||
os.path.join(utilityPath, "crashinject.exe"))
|
||||
if os.path.exists(crashinject):
|
||||
status = subprocess.Popen(
|
||||
[crashinject, str(process.pid)]).wait()
|
||||
printstatus("crashinject", status)
|
||||
if status == 0:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
process.kill(sig=signal.SIGABRT)
|
||||
except OSError:
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=921509
|
||||
self.log.info("Can't trigger Breakpad, process no longer exists")
|
||||
return
|
||||
self.log.info("Can't trigger Breakpad, just killing process")
|
||||
process.kill()
|
||||
|
||||
def runApp(self, profile, binary, cmdargs, env,
|
||||
timeout=None, debuggerInfo=None,
|
||||
symbolsPath=None, options=None,
|
||||
valgrindPath=None, valgrindArgs=None, valgrindSuppFiles=None):
|
||||
|
||||
def timeoutHandler():
|
||||
self.handleTimeout(
|
||||
timeout, proc, options.utilityPath, debuggerInfo)
|
||||
|
||||
interactive = False
|
||||
debug_args = None
|
||||
if debuggerInfo:
|
||||
interactive = debuggerInfo.interactive
|
||||
debug_args = [debuggerInfo.path] + debuggerInfo.args
|
||||
|
||||
def record_last_test(message):
|
||||
"""Records the last test seen by this harness for the benefit of crash logging."""
|
||||
if message['action'] == 'test_start':
|
||||
if " " in message['test']:
|
||||
self.lastTestSeen = message['test'].split(" ")[0]
|
||||
else:
|
||||
self.lastTestSeen = message['test']
|
||||
|
||||
self.log.add_handler(record_last_test)
|
||||
|
||||
outputHandler = OutputHandler(self.log, options.utilityPath, symbolsPath=symbolsPath)
|
||||
|
||||
kp_kwargs = {
|
||||
'kill_on_timeout': False,
|
||||
'cwd': SCRIPT_DIRECTORY,
|
||||
'onTimeout': [timeoutHandler],
|
||||
'processOutputLine': [outputHandler],
|
||||
}
|
||||
|
||||
if interactive:
|
||||
# If an interactive debugger is attached,
|
||||
# don't use timeouts, and don't capture ctrl-c.
|
||||
timeout = None
|
||||
signal.signal(signal.SIGINT, lambda sigid, frame: None)
|
||||
|
||||
if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk':
|
||||
runner_cls = mozrunner.Runner
|
||||
else:
|
||||
runner_cls = mozrunner.runners.get(mozinfo.info.get('appname', 'firefox'),
|
||||
mozrunner.Runner)
|
||||
runner = runner_cls(profile=profile,
|
||||
binary=binary,
|
||||
process_class=mozprocess.ProcessHandlerMixin,
|
||||
cmdargs=cmdargs,
|
||||
env=env,
|
||||
process_args=kp_kwargs)
|
||||
runner.start(debug_args=debug_args,
|
||||
interactive=interactive,
|
||||
outputTimeout=timeout)
|
||||
proc = runner.process_handler
|
||||
|
||||
if self.use_marionette:
|
||||
marionette_args = {
|
||||
'socket_timeout': options.marionette_socket_timeout,
|
||||
'startup_timeout': options.marionette_startup_timeout,
|
||||
'symbols_path': options.symbolsPath,
|
||||
}
|
||||
if options.marionette:
|
||||
host, port = options.marionette.split(':')
|
||||
marionette_args['host'] = host
|
||||
marionette_args['port'] = int(port)
|
||||
|
||||
marionette = Marionette(**marionette_args)
|
||||
marionette.start_session(timeout=options.marionette_port_timeout)
|
||||
|
||||
addons = Addons(marionette)
|
||||
if options.specialPowersExtensionPath:
|
||||
addons.install(options.specialPowersExtensionPath, temp=True)
|
||||
|
||||
addons.install(options.reftestExtensionPath, temp=True)
|
||||
|
||||
marionette.delete_session()
|
||||
|
||||
status = runner.wait()
|
||||
runner.process_handler = None
|
||||
|
||||
if status:
|
||||
msg = "TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s" % \
|
||||
(self.lastTestSeen, status)
|
||||
# use process_output so message is logged verbatim
|
||||
self.log.process_output(None, msg)
|
||||
else:
|
||||
self.lastTestSeen = 'Main app process exited normally'
|
||||
|
||||
crashed = mozcrash.log_crashes(self.log, os.path.join(profile.profile, 'minidumps'),
|
||||
symbolsPath, test=self.lastTestSeen)
|
||||
|
||||
runner.cleanup()
|
||||
if not status and crashed:
|
||||
status = 1
|
||||
return status
|
||||
|
||||
def runSerialTests(self, manifests, options, cmdargs=None):
|
||||
debuggerInfo = None
|
||||
if options.debugger:
|
||||
debuggerInfo = mozdebug.get_debugger_info(options.debugger, options.debuggerArgs,
|
||||
options.debuggerInteractive)
|
||||
|
||||
profileDir = None
|
||||
try:
|
||||
if cmdargs is None:
|
||||
cmdargs = []
|
||||
|
||||
if self.use_marionette:
|
||||
cmdargs.append('-marionette')
|
||||
|
||||
profile = self.createReftestProfile(options, manifests)
|
||||
profileDir = profile.profile # name makes more sense
|
||||
|
||||
# browser environment
|
||||
browserEnv = self.buildBrowserEnv(options, profileDir)
|
||||
|
||||
self.log.info("Running with e10s: {}".format(options.e10s))
|
||||
status = self.runApp(profile,
|
||||
binary=options.app,
|
||||
cmdargs=cmdargs,
|
||||
# give the JS harness 30 seconds to deal with
|
||||
# its own timeouts
|
||||
env=browserEnv,
|
||||
timeout=options.timeout + 30.0,
|
||||
symbolsPath=options.symbolsPath,
|
||||
options=options,
|
||||
debuggerInfo=debuggerInfo)
|
||||
self.log.info("Process mode: {}".format('e10s' if options.e10s else 'non-e10s'))
|
||||
mozleak.process_leak_log(self.leakLogFile,
|
||||
leak_thresholds=options.leakThresholds,
|
||||
stack_fixer=get_stack_fixer_function(options.utilityPath,
|
||||
options.symbolsPath),
|
||||
)
|
||||
finally:
|
||||
self.cleanup(profileDir)
|
||||
return status
|
||||
|
||||
def copyExtraFilesToProfile(self, options, profile):
|
||||
"Copy extra files or dirs specified on the command line to the testing profile."
|
||||
profileDir = profile.profile
|
||||
if not os.path.exists(os.path.join(profileDir, "hyphenation")):
|
||||
os.makedirs(os.path.join(profileDir, "hyphenation"))
|
||||
for f in options.extraProfileFiles:
|
||||
abspath = self.getFullPath(f)
|
||||
if os.path.isfile(abspath):
|
||||
if os.path.basename(abspath) == 'user.js':
|
||||
extra_prefs = mozprofile.Preferences.read_prefs(abspath)
|
||||
profile.set_preferences(extra_prefs)
|
||||
elif os.path.basename(abspath).endswith('.dic'):
|
||||
shutil.copy2(abspath, os.path.join(profileDir, "hyphenation"))
|
||||
else:
|
||||
shutil.copy2(abspath, profileDir)
|
||||
elif os.path.isdir(abspath):
|
||||
dest = os.path.join(profileDir, os.path.basename(abspath))
|
||||
shutil.copytree(abspath, dest)
|
||||
else:
|
||||
self.log.warning(
|
||||
"runreftest.py | Failed to copy %s to profile" % abspath)
|
||||
continue
|
||||
|
||||
|
||||
def run_test_harness(parser, options):
|
||||
reftest = RefTest()
|
||||
parser.validate(options, reftest)
|
||||
|
||||
# We have to validate options.app here for the case when the mach
|
||||
# command is able to find it after argument parsing. This can happen
|
||||
# when running from a tests.zip.
|
||||
if not options.app:
|
||||
parser.error("could not find the application path, --appname must be specified")
|
||||
|
||||
options.app = reftest.getFullPath(options.app)
|
||||
if not os.path.exists(options.app):
|
||||
parser.error("Error: Path %(app)s doesn't exist. Are you executing "
|
||||
"$objdir/_tests/reftest/runreftest.py?" % {"app": options.app})
|
||||
|
||||
if options.xrePath is None:
|
||||
options.xrePath = os.path.dirname(options.app)
|
||||
|
||||
return reftest.runTests(options.tests, options)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = reftestcommandline.DesktopArgumentsParser()
|
||||
options = parser.parse_args()
|
||||
sys.exit(run_test_harness(parser, options))
|
||||
425
layout/tools/reftest/runreftestb2g.py
Normal file
425
layout/tools/reftest/runreftestb2g.py
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
# 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.
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
if here not in sys.path:
|
||||
sys.path.insert(0, here)
|
||||
|
||||
from automation import Automation
|
||||
from b2gautomation import B2GRemoteAutomation
|
||||
from runreftestmulet import run_test_harness as run_mulet_reftests
|
||||
from output import OutputHandler
|
||||
from remotereftest import RemoteReftestResolver, ReftestServer
|
||||
from runreftest import RefTest
|
||||
import reftestcommandline
|
||||
|
||||
from marionette_harness import Marionette
|
||||
from mozdevice import DeviceManagerADB, DMError
|
||||
|
||||
|
||||
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 B2GRemoteReftest(RefTest):
|
||||
|
||||
_devicemanager = None
|
||||
use_marionette = False
|
||||
localProfile = None
|
||||
remoteApp = ''
|
||||
profile = None
|
||||
resolver_cls = RemoteReftestResolver
|
||||
|
||||
def __init__(self, automation, devicemanager, options, scriptDir):
|
||||
RefTest.__init__(self)
|
||||
self.automation = 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.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
|
||||
if profileDir:
|
||||
extensionDir = os.path.join(profileDir, 'extensions', 'staged')
|
||||
for filename in os.listdir(extensionDir):
|
||||
try:
|
||||
self._devicemanager._checkCmd(['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)
|
||||
|
||||
# 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"
|
||||
|
||||
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, manifests):
|
||||
profile = RefTest.createReftestProfile(self, options, manifests,
|
||||
server=options.remoteWebServer)
|
||||
profileDir = profile.profile
|
||||
|
||||
prefs = {}
|
||||
|
||||
# Turn off the locale picker screen
|
||||
prefs["browser.firstrun.show.localepicker"] = False
|
||||
prefs["b2g.system_startup_url"] = "app://test-container.gaiamobile.org/index.html"
|
||||
prefs["b2g.system_manifest_url"] = "app://test-container.gaiamobile.org/manifest.webapp"
|
||||
prefs["dom.ipc.tabs.disabled"] = False
|
||||
prefs["dom.mozBrowserFramesEnabled"] = True
|
||||
prefs["font.size.inflation.emPerLine"] = 0
|
||||
prefs["font.size.inflation.minTwips"] = 0
|
||||
prefs["network.dns.localDomains"] = "app://test-container.gaiamobile.org"
|
||||
prefs["reftest.browser.iframe.enabled"] = False
|
||||
prefs["reftest.remote"] = True
|
||||
|
||||
# Make sure we disable system updates
|
||||
prefs["app.update.enabled"] = False
|
||||
prefs["app.update.url"] = ""
|
||||
# Disable webapp updates
|
||||
prefs["webapps.update.enabled"] = False
|
||||
# Disable tiles also
|
||||
prefs["browser.newtabpage.directory.source"] = ""
|
||||
prefs["browser.newtabpage.directory.ping"] = ""
|
||||
|
||||
# Set the extra prefs.
|
||||
profile.set_preferences(prefs)
|
||||
|
||||
# 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._checkCmd(['remount'])
|
||||
for filename in os.listdir(extensionDir):
|
||||
self._devicemanager._checkCmd(['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
|
||||
|
||||
self.updateProfilesIni(self.remoteProfile)
|
||||
|
||||
options.profilePath = self.remoteProfile
|
||||
return profile
|
||||
|
||||
def copyExtraFilesToProfile(self, options, profile):
|
||||
profileDir = profile.profile
|
||||
RefTest.copyExtraFilesToProfile(self, options, profile)
|
||||
try:
|
||||
self._devicemanager.pushDir(profileDir, options.remoteProfile)
|
||||
except DMError:
|
||||
print "Automation Error: Failed to copy extra files to device"
|
||||
raise
|
||||
|
||||
def environment(self, **kwargs):
|
||||
return self.automation.environment(**kwargs)
|
||||
|
||||
def runApp(self, profile, binary, cmdargs, env,
|
||||
timeout=None, debuggerInfo=None,
|
||||
symbolsPath=None, options=None,
|
||||
valgrindPath=None, valgrindArgs=None, valgrindSuppFiles=None):
|
||||
|
||||
outputHandler = OutputHandler(self.log, options.utilityPath, options.symbolsPath)
|
||||
status = self.automation.runApp(None, env,
|
||||
binary,
|
||||
profile.profile,
|
||||
cmdargs,
|
||||
utilityPath=options.utilityPath,
|
||||
xrePath=options.xrePath,
|
||||
debuggerInfo=debuggerInfo,
|
||||
symbolsPath=symbolsPath,
|
||||
timeout=timeout,
|
||||
outputHandler=outputHandler)
|
||||
return status
|
||||
|
||||
|
||||
def run_test_harness(parser, options):
|
||||
if options.mulet:
|
||||
return run_mulet_reftests(parser, options)
|
||||
|
||||
auto = B2GRemoteAutomation(None, "fennec")
|
||||
|
||||
# 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.logdir:
|
||||
kwargs['logdir'] = options.logdir
|
||||
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)
|
||||
if options.adb_path:
|
||||
kwargs['adb_path'] = options.adb_path
|
||||
marionette = Marionette(**kwargs)
|
||||
auto.marionette = marionette
|
||||
|
||||
if options.emulator:
|
||||
dm = marionette.emulator.dm
|
||||
else:
|
||||
# create the DeviceManager
|
||||
kwargs = {'adbPath': options.adb_path,
|
||||
'deviceRoot': options.remoteTestRoot}
|
||||
if options.deviceIP:
|
||||
kwargs.update({'host': options.deviceIP,
|
||||
'port': options.devicePort})
|
||||
dm = DeviceManagerADB(**kwargs)
|
||||
auto.setDeviceManager(dm)
|
||||
|
||||
parser.validate_remote(options, auto)
|
||||
|
||||
# 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(here, 'b2g_start_script.js')
|
||||
auto.test_script_args = [options.remoteWebServer, options.httpPort]
|
||||
|
||||
reftest = B2GRemoteReftest(auto, dm, options, here)
|
||||
parser.validate(options, reftest)
|
||||
|
||||
logParent = os.path.dirname(options.remoteLogFile)
|
||||
dm.mkDir(logParent);
|
||||
auto.setRemoteLog(options.remoteLogFile)
|
||||
auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
|
||||
|
||||
# Hack in a symbolic link for jsreftest
|
||||
os.system("ln -s %s %s" % (os.path.join('..', 'jsreftest'), os.path.join(here, 'jsreftest')))
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
retVal = reftest.runTests(options.tests, options)
|
||||
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__":
|
||||
parser = reftestcommandline.B2GArgumentParser()
|
||||
options = parser.parse_args()
|
||||
sys.exit(run_test_harness(parser, options))
|
||||
203
layout/tools/reftest/runreftestmulet.py
Normal file
203
layout/tools/reftest/runreftestmulet.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# 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 print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
from marionette_driver import expected
|
||||
from marionette_driver.by import By
|
||||
from marionette_driver.marionette import Marionette
|
||||
from marionette_driver.wait import Wait
|
||||
|
||||
from mozprocess import ProcessHandler
|
||||
from mozrunner import FirefoxRunner
|
||||
import mozinfo
|
||||
import mozlog
|
||||
|
||||
from runreftest import RefTest
|
||||
from output import OutputHandler
|
||||
import reftestcommandline
|
||||
|
||||
|
||||
class MuletReftest(RefTest):
|
||||
build_type = "mulet"
|
||||
marionette = None
|
||||
|
||||
def __init__(self, marionette_args):
|
||||
RefTest.__init__(self)
|
||||
self.last_test = os.path.basename(__file__)
|
||||
self.marionette_args = marionette_args
|
||||
self.profile = None
|
||||
self.runner = None
|
||||
self.test_script = os.path.join(here, 'b2g_start_script.js')
|
||||
self.timeout = None
|
||||
|
||||
def run_marionette_script(self):
|
||||
self.marionette = Marionette(**self.marionette_args)
|
||||
assert(self.marionette.wait_for_port())
|
||||
self.marionette.start_session()
|
||||
if self.build_type == "mulet":
|
||||
self._wait_for_homescreen(timeout=300)
|
||||
self._unlockScreen()
|
||||
self.marionette.set_context(self.marionette.CONTEXT_CHROME)
|
||||
|
||||
if os.path.isfile(self.test_script):
|
||||
f = open(self.test_script, 'r')
|
||||
self.test_script = f.read()
|
||||
f.close()
|
||||
self.marionette.execute_script(self.test_script)
|
||||
|
||||
def run_tests(self, tests, options):
|
||||
manifests = self.resolver.resolveManifests(options, tests)
|
||||
|
||||
self.profile = self.create_profile(options, manifests,
|
||||
profile_to_clone=options.profile)
|
||||
env = self.buildBrowserEnv(options, self.profile.profile)
|
||||
|
||||
self._populate_logger(options)
|
||||
outputHandler = OutputHandler(self.log, options.utilityPath, symbolsPath=options.symbolsPath)
|
||||
|
||||
kp_kwargs = { 'processOutputLine': [outputHandler],
|
||||
'onTimeout': [self._on_timeout],
|
||||
'kill_on_timeout': False }
|
||||
|
||||
if not options.debugger:
|
||||
if not options.timeout:
|
||||
if mozinfo.info['debug']:
|
||||
options.timeout = 420
|
||||
else:
|
||||
options.timeout = 300
|
||||
self.timeout = options.timeout + 30.0
|
||||
|
||||
self.log.info("%s | Running tests: start." % os.path.basename(__file__))
|
||||
cmd, args = self.build_command_line(options.app,
|
||||
ignore_window_size=options.ignoreWindowSize,
|
||||
browser_arg=options.browser_arg)
|
||||
self.runner = FirefoxRunner(profile=self.profile,
|
||||
binary=cmd,
|
||||
cmdargs=args,
|
||||
env=env,
|
||||
process_class=ProcessHandler,
|
||||
process_args=kp_kwargs,
|
||||
symbols_path=options.symbolsPath)
|
||||
|
||||
status = 0
|
||||
try:
|
||||
self.runner.start(outputTimeout=self.timeout)
|
||||
self.log.info("%s | Application pid: %d" % (
|
||||
os.path.basename(__file__),
|
||||
self.runner.process_handler.pid))
|
||||
|
||||
# kick starts the reftest harness
|
||||
self.run_marionette_script()
|
||||
status = self.runner.wait()
|
||||
finally:
|
||||
self.runner.check_for_crashes(test_name=self.last_test)
|
||||
self.runner.cleanup()
|
||||
|
||||
if status > 0:
|
||||
self.log.testFail("%s | application terminated with exit code %s" % (
|
||||
self.last_test, status))
|
||||
elif status < 0:
|
||||
self.log.info("%s | application killed with signal %s" % (
|
||||
self.last_test, -status))
|
||||
|
||||
self.log.info("%s | Running tests: end." % os.path.basename(__file__))
|
||||
return status
|
||||
|
||||
def create_profile(self, options, manifests, profile_to_clone=None):
|
||||
profile = RefTest.createReftestProfile(self, options, manifests,
|
||||
profile_to_clone=profile_to_clone)
|
||||
|
||||
prefs = {}
|
||||
# Turn off the locale picker screen
|
||||
prefs["browser.firstrun.show.localepicker"] = False
|
||||
if not self.build_type == "mulet":
|
||||
# FIXME: With Mulet we can't set this values since Gaia won't launch
|
||||
prefs["b2g.system_startup_url"] = \
|
||||
"app://test-container.gaiamobile.org/index.html"
|
||||
prefs["b2g.system_manifest_url"] = \
|
||||
"app://test-container.gaiamobile.org/manifest.webapp"
|
||||
# Make sure we disable system updates
|
||||
prefs["app.update.enabled"] = False
|
||||
prefs["app.update.url"] = ""
|
||||
# Disable webapp updates
|
||||
prefs["webapps.update.enabled"] = False
|
||||
# Disable tiles also
|
||||
prefs["browser.newtabpage.directory.source"] = ""
|
||||
prefs["browser.newtabpage.directory.ping"] = ""
|
||||
prefs["dom.ipc.tabs.disabled"] = False
|
||||
prefs["dom.mozBrowserFramesEnabled"] = True
|
||||
prefs["font.size.inflation.emPerLine"] = 0
|
||||
prefs["font.size.inflation.minTwips"] = 0
|
||||
prefs["network.dns.localDomains"] = "app://test-container.gaiamobile.org"
|
||||
prefs["reftest.browser.iframe.enabled"] = False
|
||||
prefs["reftest.remote"] = False
|
||||
|
||||
# Set the extra prefs.
|
||||
profile.set_preferences(prefs)
|
||||
return profile
|
||||
|
||||
def build_command_line(self, app, ignore_window_size=False,
|
||||
browser_arg=None):
|
||||
cmd = os.path.abspath(app)
|
||||
args = ['-marionette']
|
||||
|
||||
if browser_arg:
|
||||
args += [browser_arg]
|
||||
|
||||
if not ignore_window_size:
|
||||
args.extend(['--screen', '800x1000'])
|
||||
|
||||
if self.build_type == "mulet":
|
||||
args += ['-chrome', 'chrome://b2g/content/shell.html']
|
||||
return cmd, args
|
||||
|
||||
def _on_timeout(self):
|
||||
msg = "%s | application timed out after %s seconds with no output"
|
||||
self.log.testFail(msg % (self.last_test, self.timeout))
|
||||
self.log.error("Force-terminating active process(es).");
|
||||
|
||||
# kill process to get a stack
|
||||
self.runner.stop(sig=signal.SIGABRT)
|
||||
|
||||
def _unlockScreen(self):
|
||||
self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
|
||||
self.marionette.import_script(os.path.abspath(
|
||||
os.path.join(__file__, os.path.pardir, "gaia_lock_screen.js")))
|
||||
self.marionette.switch_to_frame()
|
||||
self.marionette.execute_async_script('GaiaLockScreen.unlock()')
|
||||
|
||||
def _wait_for_homescreen(self, timeout):
|
||||
self.log.info("Waiting for home screen to load")
|
||||
Wait(self.marionette, timeout).until(expected.element_present(
|
||||
By.CSS_SELECTOR, '#homescreen[loading-state=false]'))
|
||||
|
||||
|
||||
def run_test_harness(parser, options):
|
||||
marionette_args = {}
|
||||
if options.marionette:
|
||||
host, port = options.marionette.split(':')
|
||||
marionette_args['host'] = host
|
||||
marionette_args['port'] = int(port)
|
||||
|
||||
reftest = MuletReftest(marionette_args)
|
||||
parser.validate(options, reftest)
|
||||
|
||||
# add a -bin suffix if b2g-bin exists, but just b2g was specified
|
||||
if options.app[-4:] != '-bin':
|
||||
if os.path.isfile("%s-bin" % options.app):
|
||||
options.app = "%s-bin" % options.app
|
||||
|
||||
if options.xrePath is None:
|
||||
options.xrePath = os.path.dirname(options.app)
|
||||
|
||||
if options.mulet and not options.profile:
|
||||
raise Exception("must specify --profile when specifying --mulet")
|
||||
|
||||
return reftest.run_tests(options.tests, options)
|
||||
101
layout/tools/tests/content_dumping.html
Normal file
101
layout/tools/tests/content_dumping.html
Normal file
|
|
@ -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_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>
|
||||
|
||||
111
layout/tools/tests/debug_utils.html
Normal file
111
layout/tools/tests/debug_utils.html
Normal file
|
|
@ -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>
|
||||
|
||||
132
layout/tools/tests/regression_tests.html
Normal file
132
layout/tools/tests/regression_tests.html
Normal file
|
|
@ -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>
|
||||
|
||||
553
layout/tools/tests/regression_tests.js
Normal file
553
layout/tools/tests/regression_tests.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue