jenkins-bot has submitted this change and it was merged.
Change subject: ve.Filibuster: Scrupulous state logging
......................................................................
ve.Filibuster: Scrupulous state logging
ve.Filibuster
* Wrap functions to log entry/exit
* Log state and stack at each function call
* Shortlist potentially significant state change observations
ve.ui.DebugBar
* Start Filibuster/Stop Filibuster button
* Tabulated observation HTML
ve.ui.Surface
* ve.Filibuster hooks
ve.ce.SurfaceObserver
* Call setTimeout through a method, to facilitate disabling
ve.EventSequencer
* Perform all calls through a method 'callListener', for clearer logs
ve.debug
* define ve.error
demos/* and others
* Minor and consequential changes
Change-Id: I559eba8bed17a7669b445d307c3866f2548f908c
---
M .docs/categories.json
M .docs/config.json
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/demo.css
M demos/ve/desktop.html
M demos/ve/mobile.html
M src/ce/ve.ce.SurfaceObserver.js
M src/ui/styles/ve.ui.DebugBar.css
M src/ui/ve.ui.DebugBar.js
M src/ui/ve.ui.Surface.js
M src/ve.EventSequencer.js
A src/ve.Filibuster.js
M src/ve.debug.js
M src/ve.js
M tests/index.html
16 files changed, 535 insertions(+), 15 deletions(-)
Approvals:
Catrope: Looks good to me, approved
jenkins-bot: Verified
diff --git a/.docs/categories.json b/.docs/categories.json
index e9675a9..1961149 100644
--- a/.docs/categories.json
+++ b/.docs/categories.json
@@ -4,7 +4,7 @@
"groups": [
{
"name": "Utilities",
- "classes": ["ve", "ve.Range",
"ve.EventSequencer"]
+ "classes": ["ve", "ve.Range",
"ve.EventSequencer", "ve.Filibuster"]
},
{
"name": "Nodes",
diff --git a/.docs/config.json b/.docs/config.json
index 9f29111..70653fb 100644
--- a/.docs/config.json
+++ b/.docs/config.json
@@ -6,7 +6,7 @@
"--warnings": ["-nodoc(class,public)"],
"--builtin-classes": true,
"--warnings-exit-nonzero": true,
- "--external": "HTMLDocument,Window,Node",
+ "--external": "HTMLDocument,Window,Node,Set",
"--output": "../docs",
"--": [
"./external.js",
diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index 3dae30d..449c622 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -124,6 +124,7 @@
<script src="../src/ve.LeafNode.js"></script>
<script src="../src/ve.Document.js"></script>
<script src="../src/ve.EventSequencer.js"></script>
+ <script src="../src/ve.Filibuster.js"></script>
<script src="../src/dm/ve.dm.js"></script>
<script src="../src/dm/ve.dm.Model.js"></script>
<script src="../src/dm/ve.dm.ModelRegistry.js"></script>
diff --git a/build/modules.json b/build/modules.json
index 4209ff7..390b3fa 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -145,6 +145,7 @@
"src/ve.LeafNode.js",
"src/ve.Document.js",
"src/ve.EventSequencer.js",
+ "src/ve.Filibuster.js",
"src/dm/ve.dm.js",
"src/dm/ve.dm.Model.js",
"src/dm/ve.dm.ModelRegistry.js",
diff --git a/demos/ve/demo.css b/demos/ve/demo.css
index 639eb12..c090295 100644
--- a/demos/ve/demo.css
+++ b/demos/ve/demo.css
@@ -86,3 +86,7 @@
.ve-ui-debugBar {
padding: 1.5em;
}
+
+.ve-ui-debugBar-filibuster {
+ padding: 1.5em;
+}
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index d428c6a..d3084d3 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -134,6 +134,7 @@
<script src="../../src/ve.LeafNode.js"></script>
<script src="../../src/ve.Document.js"></script>
<script src="../../src/ve.EventSequencer.js"></script>
+ <script src="../../src/ve.Filibuster.js"></script>
<script src="../../src/dm/ve.dm.js"></script>
<script src="../../src/dm/ve.dm.Model.js"></script>
<script src="../../src/dm/ve.dm.ModelRegistry.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index b434833..cb91992 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -135,6 +135,7 @@
<script src="../../src/ve.LeafNode.js"></script>
<script src="../../src/ve.Document.js"></script>
<script src="../../src/ve.EventSequencer.js"></script>
+ <script src="../../src/ve.Filibuster.js"></script>
<script src="../../src/dm/ve.dm.js"></script>
<script src="../../src/dm/ve.dm.Model.js"></script>
<script src="../../src/dm/ve.dm.ModelRegistry.js"></script>
diff --git a/src/ce/ve.ce.SurfaceObserver.js b/src/ce/ve.ce.SurfaceObserver.js
index 100516e..8e5bb7a 100644
--- a/src/ce/ve.ce.SurfaceObserver.js
+++ b/src/ce/ve.ce.SurfaceObserver.js
@@ -116,7 +116,10 @@
}
// only reach this point if pollOnce does not throw an exception
if ( this.frequency !== null ) {
- this.timeoutId = setTimeout( ve.bind( this.timerLoop, this ),
this.frequency );
+ this.timeoutId = this.setTimeout(
+ ve.bind( this.timerLoop, this ),
+ this.frequency
+ );
}
};
@@ -207,7 +210,7 @@
.removeClass(
've-ce-branchNode-blockSlugWrapper-focused' );
this.$slugWrapper = null;
// Emit 'position' on the surface view after the
animation completes
- setTimeout( function () {
+ this.setTimeout( function () {
if ( observer.documentView ) {
observer.documentView.documentNode.surface.emit( 'position' );
}
@@ -265,3 +268,13 @@
this.range = range;
}
};
+
+/**
+ * Wrapper for setTimeout, for ease of debugging
+ *
+ * @param {Function} callback Callback
+ * @param {number} timeout Timeout ms
+ */
+ve.ce.SurfaceObserver.prototype.setTimeout = function ( callback, timeout ) {
+ return setTimeout( callback, timeout );
+};
diff --git a/src/ui/styles/ve.ui.DebugBar.css b/src/ui/styles/ve.ui.DebugBar.css
index e876323..66f731c 100644
--- a/src/ui/styles/ve.ui.DebugBar.css
+++ b/src/ui/styles/ve.ui.DebugBar.css
@@ -90,3 +90,27 @@
.ve-ui-debugBar-dump li .ve-ui-debugBar-dump-achar {
background-color: #ffeedd;
}
+
+.ve-ui-debugBar-filibuster {
+ font-size: 0.7em;
+ margin-top: 2em;
+ display: none;
+ background-color: #f3f3f3;
+ width: 100%;
+ border: 1px solid #ddd;
+ border-radius: 0;
+ border-top-right-radius: 0.25em;
+ border-top-left-radius: 0.25em;
+}
+
+.ve-ui-debugBar-filibuster td {
+ padding: 0.25em 1em;
+ background-color: #fff;
+ vertical-align: top;
+}
+
+.ve-ui-debugBar-filibuster th {
+ padding: 0.5em 1em;
+ color: #555;
+ text-shadow: 0 1px 1px #fff;
+}
diff --git a/src/ui/ve.ui.DebugBar.js b/src/ui/ve.ui.DebugBar.js
index b4feadd..e66f1fe 100644
--- a/src/ui/ve.ui.DebugBar.js
+++ b/src/ui/ve.ui.DebugBar.js
@@ -37,6 +37,8 @@
)
);
+ this.$filibuster = this.$( '<div
class="ve-ui-debugBar-filibuster"></div>' );
+
// Widgets
this.fromTextInput = new OO.ui.TextInputWidget( { readOnly: true } );
this.toTextInput = new OO.ui.TextInputWidget( { readOnly: true } );
@@ -44,6 +46,7 @@
this.logRangeButton = new OO.ui.ButtonWidget( { label: 'Log', disabled:
true } );
this.dumpModelButton = new OO.ui.ButtonWidget( { label: 'Dump model' }
);
this.dumpModelChangeToggle = new OO.ui.ToggleButtonWidget( { label:
'Dump on change' } );
+ this.filibusterToggle = new OO.ui.ToggleButtonWidget( { label: 'Start
Filibuster' } );
var fromLabel = new OO.ui.LabelWidget(
{ label: 'Range', input: this.fromTextInput }
@@ -56,9 +59,10 @@
this.logRangeButton.on( 'click', ve.bind( this.onLogRangeButtonClick,
this ) );
this.dumpModelButton.on( 'click', ve.bind( this.onDumpModelButtonClick,
this ) );
this.dumpModelChangeToggle.on( 'click', ve.bind(
this.onDumpModelChangeToggleClick, this ) );
+ this.filibusterToggle.on( 'click', ve.bind(
this.onFilibusterToggleClick, this ) );
this.onDumpModelChangeToggleClick();
- this.getSurface().getModel().connect( this, { select:
this.onSurfaceSelect } );
+ this.getSurface().getModel().connect( this, { select: 'onSurfaceSelect'
} );
this.onSurfaceSelect( this.getSurface().getModel().getSelection() );
this.$element.addClass( 've-ui-debugBar' );
@@ -71,9 +75,11 @@
this.logRangeButton.$element,
this.$( this.constructor.static.dividerTemplate ),
this.dumpModelButton.$element,
- this.dumpModelChangeToggle.$element
+ this.dumpModelChangeToggle.$element,
+ this.filibusterToggle.$element
),
- this.$dump
+ this.$dump,
+ this.$filibuster
);
this.target = null;
@@ -241,8 +247,27 @@
ve.ui.DebugBar.prototype.onDumpModelChangeToggleClick = function () {
if ( this.dumpModelChangeToggle.getValue() ) {
this.onDumpModelButtonClick();
- this.getSurface().model.connect( this, { documentUpdate:
this.onDumpModelButtonClick } );
+ this.getSurface().model.connect( this, { documentUpdate:
'onDumpModelButtonClick' } );
} else {
- this.getSurface().model.disconnect( this, { documentUpdate:
this.onDumpModelButtonClick } );
+ this.getSurface().model.disconnect( this, { documentUpdate:
'onDumpModelButtonClick' } );
+ }
+};
+
+/**
+ * Handle click events on the filibuster toggle button
+ *
+ * @param {jQuery.Event} e Event
+ */
+ve.ui.DebugBar.prototype.onFilibusterToggleClick = function () {
+ if ( this.filibusterToggle.getValue() ) {
+ this.filibusterToggle.setLabel( 'Stop Filibuster' );
+ this.$filibuster.hide();
+ this.$filibuster.empty();
+ this.getSurface().startFilibuster();
+ } else {
+ this.getSurface().stopFilibuster();
+ this.$filibuster.html(
this.getSurface().filibuster.getObservationsHtml() );
+ this.$filibuster.show();
+ this.filibusterToggle.setLabel( 'Start Filibuster' );
}
};
diff --git a/src/ui/ve.ui.Surface.js b/src/ui/ve.ui.Surface.js
index 90a999e..7391ed1 100644
--- a/src/ui/ve.ui.Surface.js
+++ b/src/ui/ve.ui.Surface.js
@@ -4,6 +4,7 @@
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
+/*global rangy */
/**
* A surface is a top-level object which contains both a surface model and a
surface view.
@@ -48,6 +49,7 @@
this.pasteRules = {};
this.enabled = true;
this.context = this.createContext();
+ this.filibuster = null;
// Events
this.dialogs.connect( this, { closing: 'onDialogClosing' } );
@@ -393,3 +395,56 @@
ve.ui.Surface.prototype.getDir = function () {
return this.$element.css( 'direction' );
};
+
+ve.ui.Surface.prototype.initFilibuster = function () {
+ var uiSurface = this;
+ this.filibuster = new ve.Filibuster()
+ .wrapClass( ve.EventSequencer )
+ .wrapNamespace( ve.dm, 've.dm' )
+ .wrapNamespace( ve.ce, 've.ce' )
+ .wrapNamespace( ve.ui, 've.ui', [
+ // blacklist
+ ve.ui.Surface.prototype.startFilibuster,
+ ve.ui.Surface.prototype.stopFilibuster
+ ] )
+ .setObserver( 'dm doc', function () {
+ return JSON.stringify(
uiSurface.model.documentModel.data.data );
+ } )
+ .setObserver( 'dm range', function () {
+ var selection = uiSurface.model.selection;
+ if ( !selection ) {
+ return null;
+ }
+ return [ selection.from, selection.to ].join( ',' );
+ } )
+ .setObserver( 'DOM doc', function () {
+ return uiSurface.view.$element.html();
+ } )
+ .setObserver( 'DOM selection', function () {
+ var range, sel;
+ sel = rangy.getSelection(
uiSurface.view.getElementDocument() );
+ if ( sel.rangeCount === 0 ) {
+ return null;
+ }
+ range = sel.getRangeAt( 0 );
+ return JSON.stringify( {
+ startContainer: range.startContainer.outerHTML,
+ startOffset: range.startOffset,
+ endContainer: range.endContainer.outerHTML,
+ endOffset: range.endOffset
+ } );
+ } );
+};
+
+ve.ui.Surface.prototype.startFilibuster = function () {
+ if ( !this.filibuster ) {
+ this.initFilibuster();
+ } else {
+ this.filibuster.clearLogs();
+ }
+ this.filibuster.start();
+};
+
+ve.ui.Surface.prototype.stopFilibuster = function () {
+ this.filibuster.stop();
+};
diff --git a/src/ve.EventSequencer.js b/src/ve.EventSequencer.js
index 02bee64..c1e335e 100644
--- a/src/ve.EventSequencer.js
+++ b/src/ve.EventSequencer.js
@@ -251,7 +251,7 @@
// Length cache 'len' is required, as an onListener could add another
onListener
for ( i = 0, len = onListeners.length; i < len; i++ ) {
onListener = onListeners[i];
- onListener( ev );
+ this.callListener( 'on', eventName, i, onListener, ev );
}
// Queue a call to afterEvent only if there are some
// afterListeners/afterOneListeners/afterLoopListeners
@@ -295,11 +295,11 @@
( this.afterOneListenersForEvent[eventName] || [] ).length = 0;
for ( i = 0, len = afterListeners.length; i < len; i++ ) {
- afterListeners[i]( ev );
+ this.callListener( 'after', eventName, i, afterListeners[i], ev
);
}
for ( i = 0, len = afterOneListeners.length; i < len; i++ ) {
- afterOneListeners[i]( ev );
+ this.callListener( 'afterOne', eventName, i,
afterOneListeners[i], ev );
}
};
@@ -312,7 +312,7 @@
var i, len;
// Length cache 'len' is required, as the functions called may add
another listener
for ( i = 0, len = this.onLoopListeners.length; i < len; i++ ) {
- this.onLoopListeners[i]();
+ this.callListener( 'onLoop', null, i, this.onLoopListeners[i],
null );
}
};
@@ -338,11 +338,11 @@
this.afterLoopOneListeners.length = 0;
for ( i = 0, len = this.afterLoopListeners.length; i < len; i++ ) {
- this.afterLoopListeners[i]();
+ this.callListener( 'afterLoop', null, i,
this.afterLoopListeners[i], null );
}
for ( i = 0, len = this.afterLoopOneListeners.length; i < len; i++ ) {
- this.afterLoopOneListeners[i]();
+ this.callListener( 'afterLoopOne', null, i,
this.afterLoopOneListeners[i], null );
}
};
@@ -411,3 +411,15 @@
ve.EventSequencer.prototype.cancelPostponed = function ( timeoutId ) {
clearTimeout( timeoutId );
};
+
+/*
+ * Single method to perform all listener calls, for ease of debugging
+ * @param {string} timing on|after|afterOne|onLoop|afterLoop|afterLoopOne
+ * @param {string} eventName Name of the event
+ * @param {number} i The sequence of the listener
+ * @param {Function} listener The listener to call
+ * @param {jQuery.Event} ev The browser event
+ */
+ve.EventSequencer.prototype.callListener = function ( timing, eventName, i,
listener, ev ) {
+ listener( ev );
+};
diff --git a/src/ve.Filibuster.js b/src/ve.Filibuster.js
new file mode 100644
index 0000000..b28113c
--- /dev/null
+++ b/src/ve.Filibuster.js
@@ -0,0 +1,358 @@
+/*!
+ * VisualEditor Logger class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+/*global Set*/
+/**
+ * A scrupulous event logger that logs state at every function call, and
+ * shortlists potentially significant observations for strict scrutiny.
+ *
+ * Functions are wrapped to log entry/exit. This creates a comprehensive log of
+ * every watched function call (typically thousands per keystroke), together
+ * with the corresponding call stack.
+ *
+ * Observer callbacks can be registered to watch certain global values (e.g.
+ * the DOM/DM content and selection). These at every watched function
+ * entry/exit, and when there is a change (typically a few times per
keystroke),
+ * an observation is logged. Each observation shows the state change and call
+ * stack, and has a log number that points into the full call log.
+ *
+ * Function wrapping generally takes place after object initialization.
+ * Property lookups that have already happened, e.g. in prior calls to
+ * OO.EventEmitter's "connect" function, will not benefit from the wrapping. It
+ * is possible to modify "connect" to perform late binding; see ve.Debug.js for
+ * an example.
+ *
+ * This code is inspired by United States v. Carolene Products Company, 304
+ * U.S. 144 (1938), Footnote Four.
+ *
+ * @class ve.Filibuster
+ */
+
+/**
+ * @constructor
+ * @param {string[]} eventNames List of event names to listen to
+ */
+ve.Filibuster = function VeFilibuster() {
+ this.stack = [];
+ this.count = 0;
+ this.states = {};
+ this.observers = {};
+ this.observations = [];
+ this.callLog = [];
+ this.active = false;
+};
+
+OO.initClass( ve.Filibuster );
+
+/**
+ * Clears logs, without detaching observers
+ */
+ve.Filibuster.prototype.clearLogs = function () {
+ var name;
+ this.count = 0;
+ for ( name in this.states ) {
+ delete this.states[ name ];
+ }
+ this.observations.length = 0;
+ this.callLog.length = 0;
+};
+
+/**
+ * Attaches an observer callback. The callback returns a value representing
the current state,
+ * which must be a string, a number, a boolean, undefined or null (this
ensures state values
+ * are immutable and can be compared with strict equals).
+ *
+ * The observer will be called before and after every function call. An
observation is logged
+ * every time there is a difference between the current return value and the
previous one.
+ *
+ * @param {string} name The name of the observer, for display in the logs.
+ * @param {Function} callback The callback; must return
string|number|boolean|undefined|null
+ * @chainable
+ */
+ve.Filibuster.prototype.setObserver = function ( name, callback ) {
+ this.observers[ name ] = callback;
+ return this;
+};
+
+/**
+ * Calls each observer, logging an observation if a change is detected. Called
at the start
+ * and end of every monitored function call.
+ *
+ * @param {string} action The function call phase: call|return|throw
+ */
+ve.Filibuster.prototype.observe = function ( action ) {
+ var name, callback, oldState, newState;
+
+ for ( name in this.observers ) {
+ callback = this.observers[ name ];
+ oldState = this.states[ name ];
+ try {
+ newState = callback();
+ } catch ( ex ) {
+ newState = 'Error: ' + ex;
+ }
+ if ( newState && !( typeof newState ).match(
/^(string|number|boolean)$/ ) ) {
+ // Be strict about the allowed types, to ensure
immutability
+ ve.error( 'Illegal state:', newState );
+ throw new Error( 'Illegal state: ' + newState );
+ }
+
+ if ( oldState !== newState ) {
+ // Write observation
+ this.observations.push( {
+ name: name,
+ logCount: this.count,
+ oldState: oldState,
+ newState: newState,
+ stack: this.stack.slice(),
+ action: action
+ } );
+ this.states[ name ] = newState;
+ }
+ }
+};
+
+/**
+ * Log a function call. Called at the start and end of every monitored
function call.
+ *
+ * @param {string} funcName The name of the function
+ * @param {string} action The function call phase: call|return|throw
+ * @param {Array|Mixed} data The call arguments, return value or exception
+ */
+ve.Filibuster.prototype.log = function ( funcName, action, data ) {
+ var topFuncName, clonedData;
+ if ( !this.active ) {
+ return;
+ }
+ // Clone the data, to avoid anachronistic changes and for easy display
+ clonedData = this.clonePlain( data );
+ if ( action === 'call' ) {
+ // Stack only contains clonedData so outside code won't mutate
it.
+ // Therefore we'll only need to slice it to preserve a snapshot.
+ this.stack.push( { funcName: funcName, data: clonedData } );
+ }
+ this.count++;
+ this.observe( action );
+ this.callLog.push( {
+ count: this.count,
+ stack: this.stack.slice(),
+ funcName: funcName,
+ action: action,
+ data: clonedData
+ } );
+ if ( action !== 'call' ) {
+ if ( this.stack.length > 0 ) {
+ topFuncName = this.stack[ this.stack.length - 1
].funcName;
+ } else {
+ topFuncName = '(none)';
+ }
+ if ( this.stack.length === 0 || topFuncName !== funcName ) {
+ throw new Error(
+ 'Expected funcName "' + topFuncName + '", got
"' + funcName + '"'
+ );
+ }
+ this.stack.pop();
+ }
+};
+
+/**
+ * Replace a reference to a function with a wrapper that performs logging.
+ *
+ * Note that the same function can be referenced multiple times; each
reference would
+ * need wrapping separately.
+ *
+ * @param {Object} container The container with the function as a property
+ * @param {string} klassName The name of the container, for display in the logs
+ * @param {string} fnName The property name of the function in the container
+ * @chainable
+ */
+
+ve.Filibuster.prototype.wrapFunction = function ( container, klassName, fnName
) {
+ var wrapper, fn, filibuster = this,
+ fullName = ( klassName || 'unknown' ) + '.' + fnName;
+ fn = container[ fnName ];
+ wrapper = function () {
+ var returnVal,
+ fnReturned = false;
+ filibuster.log( fullName, 'call', Array.prototype.slice.call(
arguments ) );
+ try {
+ returnVal = fn.apply( this, arguments );
+ fnReturned = true;
+ return returnVal;
+ } finally {
+ if ( fnReturned ) {
+ filibuster.log( fullName, 'return', returnVal );
+ } else {
+ filibuster.log( fullName, 'throw' );
+ }
+ }
+ };
+ wrapper.wrappedFunction = fn;
+ container[ fnName ] = wrapper;
+ return this;
+};
+
+/**
+ * Wrap the functions in a class with wrappers that perform logging.
+ *
+ * @param {Object} klass The class with the function as a property
+ * @param {Function[]} [blacklist] Functions that should not be wrapped
+ * @chainable
+ */
+ve.Filibuster.prototype.wrapClass = function ( klass, blacklist ) {
+ var i, len, fnName, fn, fnNames, container;
+ container = klass.prototype;
+ fnNames = Object.getOwnPropertyNames( container );
+ for ( i = 0, len = fnNames.length; i < len; i++ ) {
+ fnName = fnNames[i];
+ if ( fnName === 'prototype' || fnName === 'constructor' ) {
+ continue;
+ }
+ fn = container[fnName];
+ if ( typeof fn !== 'function' || fn.wrappedFunction ) {
+ continue;
+ }
+ if ( blacklist && blacklist.indexOf( fn ) !== -1 ) {
+ continue;
+ }
+ this.wrapFunction( container, klass.name, fnName );
+ }
+ return this;
+};
+
+/**
+ * Recursively wrap the functions in a namespace with wrappers that perform
logging.
+ *
+ * @param {Object} ns The namespace whose functions should be wrapped
+ * @param {string} nsName The name of the namespace, for display in logs
+ * @param {Function[]} [blacklist] Functions that should not be wrapped
+ * @chainable
+ */
+ve.Filibuster.prototype.wrapNamespace = function ( ns, nsName, blacklist ) {
+ var i, len, propNames, propName, prop, isConstructor;
+ propNames = Object.getOwnPropertyNames( ns );
+ for ( i = 0, len = propNames.length; i < len; i++ ) {
+ propName = propNames[i];
+ prop = ns[propName];
+ if ( blacklist && blacklist.indexOf( prop ) !== -1 ) {
+ continue;
+ }
+ isConstructor = (
+ typeof prop === 'function' &&
+ !$.isEmptyObject( prop.prototype )
+ );
+ if ( isConstructor ) {
+ this.wrapClass( prop, blacklist );
+ } else if ( typeof prop === 'function' ) {
+ this.wrapFunction( ns, nsName, propName );
+ } else if ( $.isPlainObject( prop ) ) {
+ // might be a namespace; recurse
+ this.wrapNamespace( prop, nsName + '.' + propName,
blacklist );
+ }
+ }
+ return this;
+};
+
+/**
+ * Start logging
+ */
+ve.Filibuster.prototype.start = function () {
+ this.active = true;
+};
+
+/**
+ * Stop logging
+ */
+ve.Filibuster.prototype.stop = function () {
+ this.active = false;
+};
+
+/**
+ * get an HTML representation of the observations
+ */
+ve.Filibuster.prototype.getObservationsHtml = function () {
+ function getStackHtml( stackItem ) {
+ return ve.escapeHtml(
+ stackItem.funcName + '( ' + stackItem.data.map(
function ( x ) {
+ return JSON.stringify( x );
+ } ).join( ', ' ) + ' )'
+ );
+ }
+
+ function getObservationHtml( observation ) {
+ return ( '<tr><td>' +
+ [
+ observation.name,
+ String( observation.logCount ),
+ String( observation.oldState ),
+ String( observation.newState ),
+ String( observation.action )
+ ].map( ve.escapeHtml ).join( '</td><td>' ) +
+ '</td><td>' +
+ observation.stack.slice().reverse().map( getStackHtml
).join( '<br>' ) +
+ '</td></tr>'
+ );
+ }
+ return (
+ '<table class="ve-filibuster">' +
+ '<tr><th>Type</th><th>Log</th><th>Old State</th><th>New
State</th><th>Action</th><th>Stack</th></tr>' +
+ this.observations.map( getObservationHtml ).join( '' ) +
+ '</table>'
+ );
+};
+
+/**
+ * Get a plain-old-data deep clone of val.
+ *
+ * The resulting value is easily dumpable, and will not change if val changes.
+ *
+ * @param {Object|string|number|undefined} val Value to analyze
+ * @param {Set} [seen] Seen objects, for recursion detection
+ * @return {Object|string|number|undefined} Plain old data object
+ */
+ve.Filibuster.prototype.clonePlain = function ( val, seen ) {
+ var plainVal,
+ filibuster = this;
+ if ( seen === undefined ) {
+ seen = new Set();
+ }
+ if ( Array.isArray( val ) ) {
+ if ( seen.has( val ) ) {
+ return '...';
+ }
+ seen.add( val );
+ return val.map( function ( x ) {
+ return filibuster.clonePlain( x, seen );
+ } );
+ } else if ( typeof val === 'function' ) {
+ return '(function ' + val.name + ')';
+ } else if ( val === null ) {
+ return null;
+ } else if ( val === window ) {
+ return '(window)';
+ } else if ( typeof val !== 'object' ) {
+ return val;
+ } else if ( val.constructor === ve.Range ) {
+ return { 've.Range': [ val.from, val.to ] };
+ } else if ( val.constructor === ve.dm.Transaction ) {
+ return { 've.dm.Transaction': val.operations.map( function ( op
) {
+ return filibuster.clonePlain( op );
+ } ) };
+ } else if ( val.constructor !== Object ) {
+ // Not a plain old object
+ return '(' + ( val.constructor.name || 'unknown' ) + ')';
+ } else {
+ if ( seen.has( val ) ) {
+ return '...';
+ }
+ seen.add( val );
+ plainVal = {};
+ Object.getOwnPropertyNames( val ).forEach( function ( k ) {
+ plainVal[ k ] = filibuster.clonePlain( val[ k ], seen );
+ } );
+ return plainVal;
+ }
+};
diff --git a/src/ve.debug.js b/src/ve.debug.js
index b992061..b79d2b8 100644
--- a/src/ve.debug.js
+++ b/src/ve.debug.js
@@ -34,6 +34,19 @@
};
/**
+ * Logs error to the console.
+ *
+ * @method
+ * @param {Mixed...} [data] Data to log
+ */
+ve.error = function () {
+ // In IE9 console methods are not real functions and as such do not
inherit
+ // from Function.prototype, thus console.error.apply does not exist.
+ // However it is function-like enough that passing it to Function#apply
does work.
+ Function.prototype.apply.call( console.error, console, arguments );
+};
+
+/**
* Logs an object to the console.
*
* @method
diff --git a/src/ve.js b/src/ve.js
index 2060c31..9a64dfb 100644
--- a/src/ve.js
+++ b/src/ve.js
@@ -326,6 +326,17 @@
};
/**
+ * Log error to the console.
+ *
+ * This implementation does nothing, to add a real implmementation
ve.debug needs to be loaded.
+ *
+ * @param {Mixed...} [args] Data to log
+ */
+ ve.error = function () {
+ // don't do anything, this is just a stub
+ };
+
+ /**
* Log an object to the console.
*
* This implementation does nothing, to add a real implmementation
ve.debug needs to be loaded.
diff --git a/tests/index.html b/tests/index.html
index 7cc46d0..3af3dbe 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -84,6 +84,7 @@
<script src="../src/ve.LeafNode.js"></script>
<script src="../src/ve.Document.js"></script>
<script src="../src/ve.EventSequencer.js"></script>
+ <script src="../src/ve.Filibuster.js"></script>
<script src="../src/dm/ve.dm.js"></script>
<script src="../src/dm/ve.dm.Model.js"></script>
<script src="../src/dm/ve.dm.ModelRegistry.js"></script>
--
To view, visit https://gerrit.wikimedia.org/r/152123
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I559eba8bed17a7669b445d307c3866f2548f908c
Gerrit-PatchSet: 20
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Divec <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits