@ -1,9 +1,11 @@
import async from 'async'
import * as changeCase from 'change-case'
import Web3 from 'web3' ;
import Web3 from 'web3'
import assertionEvents from './assertionEvents'
import { RunListInterface , TestCbInterface , TestResultInterface , ResultCbInterface ,
CompiledContract , AstNode , Options , FunctionDescription , UserDocumentation } from './types'
import {
RunListInterface , TestCbInterface , TestResultInterface , ResultCbInterface ,
CompiledContract , AstNode , Options , FunctionDescription , UserDocumentation
} from './types'
/ * *
* @dev Get function name using method signature
@ -12,12 +14,12 @@ import { RunListInterface, TestCbInterface, TestResultInterface, ResultCbInterfa
* /
function getFunctionFullName ( signature : string , methodIdentifiers : Record < string , string > ) : string | null {
for ( const method in methodIdentifiers ) {
if ( signature . replace ( '0x' , '' ) === methodIdentifiers [ method ] . replace ( '0x' , '' ) ) {
return method
}
for ( const method in methodIdentifiers ) {
if ( signature . replace ( '0x' , '' ) === methodIdentifiers [ method ] . replace ( '0x' , '' ) ) {
return method
}
return null
}
return null
}
/ * *
@ -25,8 +27,8 @@ function getFunctionFullName (signature: string, methodIdentifiers: Record <stri
* @param funcABI function ABI
* /
function isConstant ( funcABI : FunctionDescription ) : boolean {
return ( funcABI . constant || funcABI . stateMutability === 'view' || funcABI . stateMutability === 'pure' )
function isConstant ( funcABI : FunctionDescription ) : boolean {
return ( funcABI . constant || funcABI . stateMutability === 'view' || funcABI . stateMutability === 'pure' )
}
/ * *
@ -34,8 +36,8 @@ function isConstant(funcABI: FunctionDescription): boolean {
* @param funcABI function ABI
* /
function isPayable ( funcABI : FunctionDescription ) : boolean {
return ( funcABI . payable || funcABI . stateMutability === 'payable' )
function isPayable ( funcABI : FunctionDescription ) : boolean {
return ( funcABI . payable || funcABI . stateMutability === 'payable' )
}
/ * *
@ -44,8 +46,8 @@ function isPayable(funcABI: FunctionDescription): boolean {
* @param name name
* /
function isNodeName ( node : AstNode , name : string ) : boolean {
return node . name === name
function isNodeName ( node : AstNode , name : string ) : boolean {
return node . name === name
}
/ * *
@ -54,8 +56,8 @@ function isNodeName(node: AstNode, name: string): boolean {
* @param type type
* /
function isNodeType ( node : AstNode , type : string ) : boolean {
return node . nodeType === type
function isNodeType ( node : AstNode , type : string ) : boolean {
return node . nodeType === type
}
/ * *
@ -64,8 +66,8 @@ function isNodeType(node: AstNode, type: string): boolean {
* @param typesList list of types
* /
function isNodeTypeIn ( node : AstNode , typesList : string [ ] ) : boolean {
return typesList . includes ( node . nodeType )
function isNodeTypeIn ( node : AstNode , typesList : string [ ] ) : boolean {
return typesList . includes ( node . nodeType )
}
/ * *
@ -76,10 +78,10 @@ function isNodeTypeIn(node: AstNode, typesList: string[]): boolean {
* /
function getOverridedSender ( userdoc : UserDocumentation , signature : string , methodIdentifiers : Record < string , string > ) : string | null {
const fullName : string | null = getFunctionFullName ( signature , methodIdentifiers )
const senderRegex = /#sender: account-+(\d)/g
const accountIndex : RegExpExecArray | null = fullName && userdoc . methods [ fullName ] ? senderRegex . exec ( userdoc . methods [ fullName ] . notice ) : null
return fullName && accountIndex ? accountIndex [ 1 ] : null
const fullName : string | null = getFunctionFullName ( signature , methodIdentifiers )
const senderRegex = /#sender: account-+(\d)/g
const accountIndex : RegExpExecArray | null = fullName && userdoc . methods [ fullName ] ? senderRegex . exec ( userdoc . methods [ fullName ] . notice ) : null
return fullName && accountIndex ? accountIndex [ 1 ] : null
}
/ * *
@ -90,10 +92,10 @@ function getOverridedSender (userdoc: UserDocumentation, signature: string, meth
* /
function getProvidedValue ( userdoc : UserDocumentation , signature : string , methodIdentifiers : Record < string , string > ) : string | null {
const fullName : string | null = getFunctionFullName ( signature , methodIdentifiers )
const valueRegex = /#value: (\d+)/g
const value : RegExpExecArray | null = fullName && userdoc . methods [ fullName ] ? valueRegex . exec ( userdoc . methods [ fullName ] . notice ) : null
return fullName && value ? value [ 1 ] : null
const fullName : string | null = getFunctionFullName ( signature , methodIdentifiers )
const valueRegex = /#value: (\d+)/g
const value : RegExpExecArray | null = fullName && userdoc . methods [ fullName ] ? valueRegex . exec ( userdoc . methods [ fullName ] . notice ) : null
return fullName && value ? value [ 1 ] : null
}
/ * *
@ -103,36 +105,36 @@ function getProvidedValue (userdoc: UserDocumentation, signature: string, method
* /
function getAvailableFunctions ( fileAST : AstNode , testContractName : string ) : string [ ] {
let funcList : string [ ] = [ ]
if ( fileAST . nodes && fileAST . nodes . length > 0 ) {
const contractAST : AstNode [ ] = fileAST . nodes . filter ( node = > isNodeName ( node , testContractName ) && isNodeType ( node , 'ContractDefinition' ) )
if ( contractAST . length > 0 && contractAST [ 0 ] . nodes ) {
const funcNodes : AstNode [ ] = contractAST [ 0 ] . nodes . filter ( node = > ( ( node . kind === "function" && isNodeType ( node , 'FunctionDefinition' ) ) || isNodeType ( node , 'FunctionDefinition' ) ) )
funcList = funcNodes . map ( node = > node . name )
}
let funcList : string [ ] = [ ]
if ( fileAST . nodes && fileAST . nodes . length > 0 ) {
const contractAST : AstNode [ ] = fileAST . nodes . filter ( node = > isNodeName ( node , testContractName ) && isNodeType ( node , 'ContractDefinition' ) )
if ( contractAST . length > 0 && contractAST [ 0 ] . nodes ) {
const funcNodes : AstNode [ ] = contractAST [ 0 ] . nodes . filter ( node = > ( ( node . kind === 'function' && isNodeType ( node , 'FunctionDefinition' ) ) || isNodeType ( node , 'FunctionDefinition' ) ) )
funcList = funcNodes . map ( node = > node . name )
}
return funcList ;
}
return funcList
}
function getAssertMethodLocation ( fileAST : AstNode , testContractName : string , functionName : string , assertMethod : string ) : string {
if ( fileAST . nodes ? . length ) {
const contractAST : AstNode = fileAST . nodes . find ( node = > isNodeName ( node , testContractName ) && isNodeType ( node , 'ContractDefinition' ) )
if ( contractAST ? . nodes ? . length ) {
const funcNode : AstNode = contractAST . nodes . find ( node = > isNodeName ( node , functionName ) && isNodeType ( node , 'FunctionDefinition' ) )
// Check if statement nodeType is 'ExpressionStatement' or 'Return', for examples:
// Assert.equal(foo.get(), 100, "initial value is not correct");
// return Assert.equal(foo.get(), 100, "initial value is not correct");
const expressions = funcNode . body . statements . filter ( s = >
isNodeTypeIn ( s , [ 'ExpressionStatement' , 'Return' ] )
&& isNodeType ( s . expression , 'FunctionCall' ) )
const assetExpression = expressions . find ( e = > e . expression . expression
&& isNodeType ( e . expression . expression , 'MemberAccess' )
&& e . expression . expression . memberName === assertMethod
&& isNodeName ( e . expression . expression . expression , 'Assert' )
)
return assetExpression ? . expression ? . src
}
if ( fileAST . nodes ? . length ) {
const contractAST : AstNode = fileAST . nodes . find ( node = > isNodeName ( node , testContractName ) && isNodeType ( node , 'ContractDefinition' ) )
if ( contractAST ? . nodes ? . length ) {
const funcNode : AstNode = contractAST . nodes . find ( node = > isNodeName ( node , functionName ) && isNodeType ( node , 'FunctionDefinition' ) )
// Check if statement nodeType is 'ExpressionStatement' or 'Return', for examples:
// Assert.equal(foo.get(), 100, "initial value is not correct");
// return Assert.equal(foo.get(), 100, "initial value is not correct");
const expressions = funcNode . body . statements . filter ( s = >
isNodeTypeIn ( s , [ 'ExpressionStatement' , 'Return' ] ) &&
isNodeType ( s . expression , 'FunctionCall' ) )
const assetExpression = expressions . find ( e = > e . expression . expression &&
isNodeType ( e . expression . expression , 'MemberAccess' ) &&
e . expression . expression . memberName === assertMethod &&
isNodeName ( e . expression . expression . expression , 'Assert' )
)
return assetExpression ? . expression ? . src
}
}
}
/ * *
@ -142,15 +144,15 @@ function getAssertMethodLocation (fileAST: AstNode, testContractName: string, fu
* /
function getTestFunctionsInterface ( jsonInterface : FunctionDescription [ ] , funcList : string [ ] ) : FunctionDescription [ ] {
const functionsInterface : FunctionDescription [ ] = [ ]
const specialFunctions : string [ ] = [ 'beforeAll' , 'beforeEach' , 'afterAll' , 'afterEach' ]
for ( const func of funcList ) {
if ( ! specialFunctions . includes ( func ) ) {
const funcInterface : FunctionDescription | undefined = jsonInterface . find ( node = > node . type === 'function' && node . name === func )
if ( funcInterface ) functionsInterface . push ( funcInterface )
}
const functionsInterface : FunctionDescription [ ] = [ ]
const specialFunctions : string [ ] = [ 'beforeAll' , 'beforeEach' , 'afterAll' , 'afterEach' ]
for ( const func of funcList ) {
if ( ! specialFunctions . includes ( func ) ) {
const funcInterface : FunctionDescription | undefined = jsonInterface . find ( node = > node . type === 'function' && node . name === func )
if ( funcInterface ) functionsInterface . push ( funcInterface )
}
return functionsInterface
}
return functionsInterface
}
/ * *
@ -159,15 +161,15 @@ function getTestFunctionsInterface (jsonInterface: FunctionDescription[], funcLi
* /
function getSpecialFunctionsInterface ( jsonInterface : FunctionDescription [ ] ) : Record < string , FunctionDescription > {
const specialFunctionsInterface : Record < string , FunctionDescription > = { }
const funcList : string [ ] = [ 'beforeAll' , 'beforeEach' , 'afterAll' , 'afterEach' ]
for ( const func of funcList ) {
const funcInterface : FunctionDescription | undefined = jsonInterface . find ( node = > node . type === 'function' && node . name === func )
if ( funcInterface ) {
specialFunctionsInterface [ func ] = funcInterface
}
const specialFunctionsInterface : Record < string , FunctionDescription > = { }
const funcList : string [ ] = [ 'beforeAll' , 'beforeEach' , 'afterAll' , 'afterEach' ]
for ( const func of funcList ) {
const funcInterface : FunctionDescription | undefined = jsonInterface . find ( node = > node . type === 'function' && node . name === func )
if ( funcInterface ) {
specialFunctionsInterface [ func ] = funcInterface
}
return specialFunctionsInterface
}
return specialFunctionsInterface
}
/ * *
@ -178,184 +180,182 @@ function getSpecialFunctionsInterface (jsonInterface: FunctionDescription[]): Re
* /
function createRunList ( jsonInterface : FunctionDescription [ ] , fileAST : AstNode , testContractName : string ) : RunListInterface [ ] {
const availableFunctions : string [ ] = getAvailableFunctions ( fileAST , testContractName )
const testFunctionsInterface : FunctionDescription [ ] = getTestFunctionsInterface ( jsonInterface , availableFunctions )
const specialFunctionsInterface : Record < string , FunctionDescription > = getSpecialFunctionsInterface ( jsonInterface )
const runList : RunListInterface [ ] = [ ]
if ( availableFunctions . includes ( 'beforeAll' ) ) {
const func = specialFunctionsInterface [ 'beforeAll' ]
runList . push ( { name : 'beforeAll' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
const availableFunctions : string [ ] = getAvailableFunctions ( fileAST , testContractName )
const testFunctionsInterface : FunctionDescription [ ] = getTestFunctionsInterface ( jsonInterface , availableFunctions )
const specialFunctionsInterface : Record < string , FunctionDescription > = getSpecialFunctionsInterface ( jsonInterface )
const runList : RunListInterface [ ] = [ ]
if ( availableFunctions . includes ( 'beforeAll' ) ) {
const func = specialFunctionsInterface [ 'beforeAll' ]
runList . push ( { name : 'beforeAll' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
for ( const func of testFunctionsInterface ) {
if ( availableFunctions . includes ( 'beforeEach' ) ) {
const func = specialFunctionsInterface [ 'beforeEach' ]
runList . push ( { name : 'beforeEach' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
for ( const func of testFunctionsInterface ) {
if ( availableFunctions . includes ( 'beforeEach' ) ) {
const func = specialFunctionsInterface [ 'beforeEach' ]
runList . push ( { name : 'beforeEach' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
if ( func . name && func . inputs ) runList . push ( { name : func.name , inputs : func.inputs , signature : func.signature , type : 'test' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
if ( availableFunctions . indexOf ( 'afterEach' ) >= 0 ) {
const func = specialFunctionsInterface [ 'afterEach' ]
runList . push ( { name : 'afterEach' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
if ( func . name && func . inputs ) runList . push ( { name : func.name , inputs : func.inputs , signature : func.signature , type : 'test' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
if ( availableFunctions . indexOf ( 'afterEach' ) >= 0 ) {
const func = specialFunctionsInterface [ 'afterEach' ]
runList . push ( { name : 'afterEach' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
}
if ( availableFunctions . indexOf ( 'afterAll' ) >= 0 ) {
const func = specialFunctionsInterface [ 'afterAll' ]
runList . push ( { name : 'afterAll' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
if ( availableFunctions . indexOf ( 'afterAll' ) >= 0 ) {
const func = specialFunctionsInterface [ 'afterAll' ]
runList . push ( { name : 'afterAll' , inputs : func.inputs , signature : func.signature , type : 'internal' , constant : isConstant ( func ) , payable : isPayable ( func ) } )
}
return runList
return runList
}
export function runTest ( testName : string , testObject : any , contractDetails : CompiledContract , fileAST : AstNode , opts : Options , testCallback : TestCbInterface , resultsCallback : ResultCbInterface ) : void {
let passingNum = 0
let failureNum = 0
let timePassed = 0
const isJSONInterfaceAvailable = testObject && testObject . options && testObject . options . jsonInterface
if ( ! isJSONInterfaceAvailable )
return resultsCallback ( new Error ( 'Contract interface not available' ) , { passingNum , failureNum , timePassed } )
const runList : RunListInterface [ ] = createRunList ( testObject . options . jsonInterface , fileAST , testName )
const web3 = new Web3 ( )
const accts : TestResultInterface = {
type : 'accountList' ,
value : opts.accounts
}
testCallback ( undefined , accts )
const resp : TestResultInterface = {
type : 'contract' ,
value : testName ,
filename : testObject.filename
let passingNum = 0
let failureNum = 0
let timePassed = 0
const isJSONInterfaceAvailable = testObject && testObject . options && testObject . options . jsonInterface
if ( ! isJSONInterfaceAvailable ) { return resultsCallback ( new Error ( 'Contract interface not available' ) , { passingNum , failureNum , timePassed } ) }
const runList : RunListInterface [ ] = createRunList ( testObject . options . jsonInterface , fileAST , testName )
const web3 = new Web3 ( )
const accts : TestResultInterface = {
type : 'accountList' ,
value : opts.accounts
}
testCallback ( undefined , accts )
const resp : TestResultInterface = {
type : 'contract' ,
value : testName ,
filename : testObject.filename
}
testCallback ( undefined , resp )
async . eachOfLimit ( runList , 1 , function ( func , index , next ) {
let sender : string | null = null
if ( func . signature ) {
sender = getOverridedSender ( contractDetails . userdoc , func . signature , contractDetails . evm . methodIdentifiers )
if ( opts . accounts && sender ) {
sender = opts . accounts [ sender ]
}
}
testCallback ( undefined , resp )
async . eachOfLimit ( runList , 1 , function ( func , index , next ) {
let sender : string | null = null
if ( func . signature ) {
sender = getOverridedSender ( contractDetails . userdoc , func . signature , contractDetails . evm . methodIdentifiers )
if ( opts . accounts && sender ) {
sender = opts . accounts [ sender ]
}
}
let sendParams : Record < string , string > | null = null
if ( sender ) sendParams = { from : sender }
if ( func . inputs && func . inputs . length > 0 )
return resultsCallback ( new Error ( ` Method ' ${ func . name } ' can not have parameters inside a test contract ` ) , { passingNum , failureNum , timePassed } )
const method = testObject . methods [ func . name ] . apply ( testObject . methods [ func . name ] , [ ] )
const startTime = Date . now ( )
if ( func . constant ) {
method . call ( sendParams ) . then ( ( result ) = > {
const time = ( Date . now ( ) - startTime ) / 1000.0
if ( result ) {
const resp : TestResultInterface = {
type : 'testPass' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
context : testName
}
testCallback ( undefined , resp )
passingNum += 1
timePassed += time
} else {
const resp : TestResultInterface = {
type : 'testFailure' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
errMsg : 'function returned false' ,
context : testName
}
testCallback ( undefined , resp )
failureNum += 1
timePassed += time
}
next ( )
} )
let sendParams : Record < string , string > | null = null
if ( sender ) sendParams = { from : sender }
if ( func . inputs && func . inputs . length > 0 ) { return resultsCallback ( new Error ( ` Method ' ${ func . name } ' can not have parameters inside a test contract ` ) , { passingNum , failureNum , timePassed } ) }
const method = testObject . methods [ func . name ] . apply ( testObject . methods [ func . name ] , [ ] )
const startTime = Date . now ( )
if ( func . constant ) {
method . call ( sendParams ) . then ( ( result ) = > {
const time = ( Date . now ( ) - startTime ) / 1000.0
if ( result ) {
const resp : TestResultInterface = {
type : 'testPass' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
context : testName
}
testCallback ( undefined , resp )
passingNum += 1
timePassed += time
} else {
if ( func . payable ) {
const value = getProvidedValue ( contractDetails . userdoc , func . signature , contractDetails . evm . methodIdentifiers )
if ( value ) {
if ( sendParams ) sendParams . value = value
else sendParams = { value }
}
}
method . send ( sendParams ) . on ( 'receipt' , ( receipt ) = > {
try {
const time : number = ( Date . now ( ) - startTime ) / 1000.0
const assertionEventHashes = assertionEvents . map ( e = > Web3 . utils . sha3 ( e . name + '(' + e . params . join ( ) + ')' ) )
let testPassed = false
for ( const i in receipt . events ) {
let events = receipt . events [ i ]
if ( ! Array . isArray ( events ) ) events = [ events ]
for ( const event of events ) {
const eIndex = assertionEventHashes . indexOf ( event . raw . topics [ 0 ] ) // event name topic will always be at index 0
if ( eIndex >= 0 ) {
const testEvent = web3 . eth . abi . decodeParameters ( assertionEvents [ eIndex ] . params , event . raw . data )
if ( ! testEvent [ 0 ] ) {
const assertMethod = testEvent [ 2 ]
if ( assertMethod === 'ok' ) { // for 'Assert.ok' method
testEvent [ 3 ] = 'false'
testEvent [ 4 ] = 'true'
}
const location = getAssertMethodLocation ( fileAST , testName , func . name , assertMethod )
const resp : TestResultInterface = {
type : 'testFailure' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
errMsg : testEvent [ 1 ] ,
context : testName ,
assertMethod ,
returned : testEvent [ 3 ] ,
expected : testEvent [ 4 ] ,
location
} ;
testCallback ( undefined , resp )
failureNum += 1
timePassed += time
return next ( )
}
testPassed = true
}
}
}
if ( testPassed ) {
const resp : TestResultInterface = {
type : 'testPass' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
context : testName
}
testCallback ( undefined , resp )
passingNum += 1
timePassed += time
}
return next ( )
} catch ( err ) {
console . error ( err )
return next ( err )
}
} ) . on ( 'error' , function ( err : Error ) {
const time : number = ( Date . now ( ) - startTime ) / 1000.0
const resp : TestResultInterface = {
const resp : TestResultInterface = {
type : 'testFailure' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
errMsg : 'function returned false' ,
context : testName
}
testCallback ( undefined , resp )
failureNum += 1
timePassed += time
}
next ( )
} )
} else {
if ( func . payable ) {
const value = getProvidedValue ( contractDetails . userdoc , func . signature , contractDetails . evm . methodIdentifiers )
if ( value ) {
if ( sendParams ) sendParams . value = value
else sendParams = { value }
}
}
method . send ( sendParams ) . on ( 'receipt' , ( receipt ) = > {
try {
const time : number = ( Date . now ( ) - startTime ) / 1000.0
const assertionEventHashes = assertionEvents . map ( e = > Web3 . utils . sha3 ( e . name + '(' + e . params . join ( ) + ')' ) )
let testPassed = false
for ( const i in receipt . events ) {
let events = receipt . events [ i ]
if ( ! Array . isArray ( events ) ) events = [ events ]
for ( const event of events ) {
const eIndex = assertionEventHashes . indexOf ( event . raw . topics [ 0 ] ) // event name topic will always be at index 0
if ( eIndex >= 0 ) {
const testEvent = web3 . eth . abi . decodeParameters ( assertionEvents [ eIndex ] . params , event . raw . data )
if ( ! testEvent [ 0 ] ) {
const assertMethod = testEvent [ 2 ]
if ( assertMethod === 'ok' ) { // for 'Assert.ok' method
testEvent [ 3 ] = 'false'
testEvent [ 4 ] = 'true'
}
const location = getAssertMethodLocation ( fileAST , testName , func . name , assertMethod )
const resp : TestResultInterface = {
type : 'testFailure' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
errMsg : err.message ,
context : testName
} ;
errMsg : testEvent [ 1 ] ,
context : testName ,
assertMethod ,
returned : testEvent [ 3 ] ,
expected : testEvent [ 4 ] ,
location
}
testCallback ( undefined , resp )
failureNum += 1
timePassed += time
return next ( )
} )
return next ( )
}
testPassed = true
}
}
}
if ( testPassed ) {
const resp : TestResultInterface = {
type : 'testPass' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
context : testName
}
testCallback ( undefined , resp )
passingNum += 1
timePassed += time
}
return next ( )
} catch ( err ) {
console . error ( err )
return next ( err )
}
} , function ( error ) {
resultsCallback ( error , { passingNum , failureNum , timePassed } )
} )
} ) . on ( 'error' , function ( err : Error ) {
const time : number = ( Date . now ( ) - startTime ) / 1000.0
const resp : TestResultInterface = {
type : 'testFailure' ,
value : changeCase.sentenceCase ( func . name ) ,
filename : testObject.filename ,
time : time ,
errMsg : err.message ,
context : testName
}
testCallback ( undefined , resp )
failureNum += 1
timePassed += time
return next ( )
} )
}
} , function ( error ) {
resultsCallback ( error , { passingNum , failureNum , timePassed } )
} )
}