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

Reply via email to