1
0
Fork 0
easejs/test/ctor/ErrorCtorTest.js

558 lines
17 KiB
JavaScript

/**
* Tests error constructor generation
*
* Copyright (C) 2016 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
* ease.js is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function DummyError() {};
function SubDummyError() {};
SubDummyError.prototype = new DummyError();
SubDummyError.prototype.name = 'sub dummy name';
require( 'common' ).testCase(
{
caseSetUp: function()
{
this.Sut = this.require( 'ctor/ErrorCtor' );
// no need to be comprehensive
this.bases = [ '', {} ];
// space is intentional, since in IceCat (for example) you get
// '@debugger eval code' if you get an error in the console
this.frames = [
// average case, multiple frames, Mozilla-style
{
frames: [
"__$$ector$$__@foo.js:1:2",
"bar@first-frame.js:1:2",
"baz@second other frame:2:2",
],
strip: 1,
fileName: 'first-frame.js',
lineNumber: 1,
columnNumber: 2,
},
// average case, multiple frames, Mozilla-style, no-match
{
frames: [
"__$$no$$__@foo.js:1:2",
"bar@first-frame2.js:1:2",
"baz@second other frame:2:2",
],
strip: 0,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
},
// average case, Mozilla-style, no column number
{
frames: [
"__$$ector$$__@foo.js:1",
"bar@first-frame2.js:2",
"baz@second other frame:3",
],
strip: 1,
fileName: 'first-frame2.js',
lineNumber: 2,
columnNumber: undefined,
},
// average case, Mozilla-style, no frame name
{
frames: [
"__$$ector$$__@foo.js:1:1",
"@first-frame3.js:2:2",
"baz@second other frame:3:3",
],
strip: 1,
fileName: 'first-frame3.js',
lineNumber: 2,
columnNumber: 2,
},
// no other frames (but notice the trailing
// newline), Mozilla-style
{
frames: [
"__$$ector$$__@foo.js:1:2\n",
],
strip: 1,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
},
// average case, multiple frames, V8-style
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1:2)",
" at bar (first-frame.js:1:2)",
" at baz (second other frame:2:2)",
],
strip: 2,
fileName: 'first-frame.js',
lineNumber: 1,
columnNumber: 2,
},
// average case, V8-style, multiple frames, no-match
{
frames: [
"SomeError",
" at __$$nomatch$$__ (foo.js:1:2)",
" at bar (first-frame.js:1:2)",
" at baz (second other frame:2:2)",
],
strip: 0,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
},
// average case, V8-style, no column number
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1)",
" at bar (first-frame2.js:1)",
" at baz (second other frame:2)",
],
strip: 2,
fileName: 'first-frame2.js',
lineNumber: 1,
columnNumber: undefined,
},
// average case, V8-style, no frame name
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1:2)",
" at (first-frame.js:1:2)",
" at (second other frame:2:2)",
],
strip: 2,
fileName: 'first-frame.js',
lineNumber: 1,
columnNumber: 2,
},
// no other frames, V8-style
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1:2)"
],
strip: 2,
fileName: 'first-frame.js',
lineNumber: undefined,
columnNumber: undefined,
},
];
// whether line, column, or filename are available in environment
this.linecolf = [
{ lineNumber: false, columnNumber: false, fileName: true },
{ lineNumber: true, columnNumber: false, fileName: true },
{ lineNumber: false, columnNumber: true, fileName: true },
{ lineNumber: true , columnNumber: true, fileName: true },
{ lineNumber: false, columnNumber: false, fileName: false },
{ lineNumber: true, columnNumber: false, fileName: false },
{ lineNumber: false, columnNumber: true, fileName: false },
{ lineNumber: true , columnNumber: true, fileName: false },
];
},
'@each(bases) Throws error if base is not a function': function( obj )
{
this.assertThrows( function()
{
this.Sut( obj );
}, TypeError );
},
'@each(bases) Throws error if supertype is not a function': function( obj )
{
this.assertThrows( function()
{
this.Sut( DummyError, obj );
}, TypeError );
},
/**
* Error messages are set by a `message' property.
*/
'Sets message via constructor': function()
{
var expected = 'foo message';
this.assertEqual(
new ( this.Sut( DummyError ).createCtor( DummyError ) )
( expected ).message,
expected
);
},
/**
* The build-in ECMAScript Error constructors don't cast MESSAGE to a
* string, so we shouldn't either.
*/
'Does not cast message to string': function()
{
var expected = {};
this.assertStrictEqual(
new ( this.Sut( DummyError ).createCtor( DummyError ) )
( expected ).message,
expected
);
},
/**
* The name of the error is derived from the `name' property.
*/
'Sets name to class name': function( Type )
{
var expected = 'MyError';
this.assertEqual(
new (
this.Sut( DummyError ).createCtor( DummyError, expected )
)().name,
expected
);
},
/**
* ...unless one is not provided, in which case we should retain the
* parent's.
*
* Since the constructor generator doesn't set up the constructor's
* supertype, this amounts to seeing if it'll fall through if we set the
* supertype. We don't want to just check whether `name' is or is not
* defined on the prototype, since we only care that it works, not how
* it's done.
*/
'Defaults name to supertype': function( Type )
{
var ctor = this.Sut( DummyError )
.createCtor( SubDummyError );
ctor.prototype = new SubDummyError();
this.assertEqual(
new ctor().name,
new SubDummyError().name
);
},
/**
* JavaScript doesn't make extending Error pleasent---we need to take
* care of our own stack trace, and that trace isn't going to be
* entirely correct (because we have an extra stack frame, being in the
* Error itself). Furthermore, not all environments support stack.
*
* To make matters worse, the proper method of obtaining or overwriting
* a stack trace also varies. So, let's emulate some environments.
*/
/**
* Certain browsers (like Chromium) support `Error.captureStackTrace',
* which sets the `stack' property on the given object to either a
* complete stack trace, or a stack trace below a reference to a given
* object. The `stack' property is also defined as a getter, which
* makes for confusing and frustrating development when you are
* wondering why setting it does nothing. (Personal experience
* perhaps?)
*
* Note that the previous tests will implicitly test that
* `captureStackTrace' is _not_ called when unavailable, because they
* will fail to call an undefined function.
*/
'Uses Error.captureStackTrace when available': function()
{
var _self = this,
expected = 'as expected',
capture_args;
function DummyErrorCapture() {};
DummyErrorCapture.captureStackTrace = function()
{
capture_args = arguments;
};
var given_ctor = this.Sut( DummyErrorCapture )
.createCtor( DummyError );
// if the stack trace were generated now, then that would be bad (as
// it would be incorrect when the error is actually instantiated)
this.assertEqual( undefined, capture_args );
var inst = new given_ctor();
// destination for `stack' property set
this.assertStrictEqual( capture_args[ 0 ], inst );
// relative stack frame
this.assertStrictEqual( capture_args[ 1 ], given_ctor );
},
/**
* If `Error.captureStackTrace' is _not_ available, we fall back to the
* good-ol'-fashion overwrite-stack-with-a-super-instance-stack
* approach, which is conventional.
*/
'Overwrites `stack\' property if no `captureStackTrace\'': function()
{
var expected = 'as expected',
allow = false;
// just something that we can mock the stack on
function SubDummyError()
{
if ( !allow ) return;
this.stack = expected;
}
// this is why stack traces are traditionally a problem unless you
// remember to explicitly set it; ease.js does it for you
SubDummyError.prototype = new DummyError();
SubDummyError.prototype.stack = 'stack not set';
var given = this.Sut( DummyError )
.createCtor( SubDummyError );
// ensures that this stack is actually from a new object, not the
// stack that was set on the prototype
allow = true;
var result = new given().stack;
this.assertEqual( result, expected );
},
/**
* If `Error.captureStackTrace' is available and used, then the error
* constructor itself will not appear in the stack trace. If we have to
* set it, however, then it will---this is a consequence of
* instantiating the supertype within the error constructor in order to
* get a proper stack trace.
*
* We will attempt to strip ourselves from the string if the stack trace
* string meets certain critiera.
*
* Also make sure we don't strip if there is a non-match. Generally
* speaking, this won't often (if ever) be the case in practice, but
* let's never make assumptions.
*/
'@each(frames) Strips self from stack if no `captureStackTrace\'':
function( framedata )
{
var lines = Array.prototype.slice.call( framedata.frames );
function SubDummyError()
{
this.stack = lines.join( '\n' );
}
var ctor = this.Sut( DummyError )
.createCtor( SubDummyError );
var result = new ctor().stack;
this.assertEqual(
lines.slice( framedata.strip ).join( '\n' ),
result
);
},
/**
* Certain browsers (like GNU IceCat) support `{line,column}Number` and
* `fileName`; if those are defined, we will propagate them.
*
* ...but there's a caveat: the values set on the supertype's error
* object aren't going to be correct, because the first frame is not our
* own. That means we have to do some string parsing on the second
* frame; this will only happen if stack stripping was successful, since
* we otherwise have no idea if the second frame is actually what we
* want.
*
* Even if we do happen to know the values, if the environment in which
* we are running does not normally provide those data, then neither
* will we (for strict consistency).
*/
'@each(linecolf) Sets line, column, and filename data if available':
function( linecolf )
{
var expected = {
lineNumber: 5,
columnNumber: 3,
fileName: 'foofile.js',
}
var lines = [
"@__$$ector$$__ foo:1:1",
"@" + expected.fileName + ":" + expected.lineNumber
+ ":" + expected.columnNumber,
"@second other frame:2:2"
];
function LineColDummyError()
{
this.stack = lines.join( '\n' );
for ( var prop in linecolf )
{
if ( !linecolf[ prop ] ) continue;
// purposefully not an integer; should apply if the key
// exists at all
this[ prop ] = undefined;
}
}
var ctor = this.Sut( LineColDummyError )
.createCtor( LineColDummyError );
var errobj = new ctor();
for ( var prop in linecolf )
{
if ( linecolf[ prop ] )
{
this.assertEqual( expected[ prop ], errobj[ prop ] );
}
else
{
this.assertOk(
!Object.hasOwnProperty.call( errobj, prop )
);
}
}
},
/**
* This tests various situations with regards to the data available in
* stack traces; see `this.frames` for those cases.
*/
'@each(frames) Recognizes line, column, and filename when available':
function( framedata )
{
function LineColDummyError()
{
this.stack = framedata.frames.join( '\n' );
this.lineNumber = undefined;
this.columnNumber = undefined;
this.fileName = undefined;
}
var ctor = this.Sut( LineColDummyError )
.createCtor( LineColDummyError );
var errobj = new ctor();
this.assertEqual( framedata.lineNumber, errobj.lineNumber );
this.assertEqual( framedata.columnNumber, errobj.columnNumber );
},
/**
* A predicate is provided to allow callers to determine if the given
* object is our base constructor or a subtype thereof.
*/
'Provides predicate to recognize base match': function()
{
var sut = this.Sut( DummyError );
this.assertOk( sut.isError( DummyError ) );
this.assertOk( !sut.isError( new DummyError() ) );
this.assertOk( sut.isError( SubDummyError ) );
this.assertOk( !sut.isError( new SubDummyError() ) );
this.assertOk( !sut.isError( function() {} ) );
},
/**
* A function may optionally be provided to be invoked after the
* constructor has completed---this allows for the constructor to be
* augmented in such a way that the top stack frame is still the
* generated constructor when the error is instantiated.
*/
'Invokes provided function after self': function()
{
var called = false,
context = undefined,
argchk = {},
message = 'stillrunctor';
var result = new (
this.Sut( DummyError )
.createCtor( DummyError, '', function()
{
called = arguments;
context = this;
} )
)( message, argchk );
this.assertOk( called );
this.assertStrictEqual( argchk, called[ 1 ] );
this.assertStrictEqual( result, context );
// the ctor itself should also still be called (this depends on
// previous test also succeeding)
this.assertEqual( message, result.message );
},
/**
* Don't wait until instantiation to blow up on an invalid AFTER.
*/
'Throws error given a non-function `after\' argument': function()
{
var Sut = this.Sut;
this.assertThrows( function()
{
Sut( DummyError )
.createCtor( DummyError, '', "oops" );
}, TypeError );
},
} );