UNPKG

11.3 kBPlain TextView Raw
1import async from 'async'
2import * as changeCase from 'change-case'
3import Web3 from 'web3';
4import { RunListInterface, TestCbInterface, TestResultInterface, ResultCbInterface,
5 CompiledContract, AstNode, Options, FunctionDescription, UserDocumentation } from './types'
6
7/**
8 * @dev Get function name using method signature
9 * @param signature siganture
10 * @param methodIdentifiers Object containing all methods identifier
11 */
12
13function getFunctionFullName (signature: string, methodIdentifiers: Record <string, string>): string | null {
14 for (const method in methodIdentifiers) {
15 if (signature.replace('0x', '') === methodIdentifiers[method].replace('0x', '')) {
16 return method
17 }
18 }
19 return null
20}
21
22/**
23 * @dev Check if function is constant using function ABI
24 * @param funcABI function ABI
25 */
26
27function isConstant(funcABI: FunctionDescription): boolean {
28 return (funcABI.constant || funcABI.stateMutability === 'view' || funcABI.stateMutability === 'pure')
29}
30
31/**
32 * @dev Check if function is payable using function ABI
33 * @param funcABI function ABI
34 */
35
36function isPayable(funcABI: FunctionDescription): boolean {
37 return (funcABI.payable || funcABI.stateMutability === 'payable')
38}
39
40/**
41 * @dev Get overrided sender provided using natspec
42 * @param userdoc method user documentaion
43 * @param signature signature
44 * @param methodIdentifiers Object containing all methods identifier
45 */
46
47function getOverridedSender (userdoc: UserDocumentation, signature: string, methodIdentifiers: Record <string, string>): string | null {
48 const fullName: string | null = getFunctionFullName(signature, methodIdentifiers)
49 const senderRegex: RegExp = /#sender: account-+(\d)/g
50 const accountIndex: RegExpExecArray | null = fullName && userdoc.methods[fullName] ? senderRegex.exec(userdoc.methods[fullName].notice) : null
51 return fullName && accountIndex ? accountIndex[1] : null
52}
53
54/**
55 * @dev Get value provided using natspec
56 * @param userdoc method user documentaion
57 * @param signature signature
58 * @param methodIdentifiers Object containing all methods identifier
59 */
60
61function getProvidedValue (userdoc: UserDocumentation, signature: string, methodIdentifiers: Record <string, string>): string | null {
62 const fullName: string | null = getFunctionFullName(signature, methodIdentifiers)
63 const valueRegex: RegExp = /#value: (\d+)/g
64 const value: RegExpExecArray | null = fullName && userdoc.methods[fullName] ? valueRegex.exec(userdoc.methods[fullName].notice) : null
65 return fullName && value ? value[1] : null
66}
67
68/**
69 * @dev returns functions of a test contract file in same sequence they appear in file (using passed AST)
70 * @param fileAST AST of test contract file source
71 * @param testContractName Name of test contract
72 */
73
74function getAvailableFunctions (fileAST: AstNode, testContractName: string): string[] {
75 let funcList: string[] = []
76 if(fileAST.nodes && fileAST.nodes.length > 0) {
77 const contractAST: AstNode[] = fileAST.nodes.filter(node => node.name === testContractName && node.nodeType === 'ContractDefinition')
78 if(contractAST.length > 0 && contractAST[0].nodes) {
79 const funcNodes: AstNode[] = contractAST[0].nodes.filter(node => ((node.kind === "function" && node.nodeType === "FunctionDefinition") || (node.nodeType === "FunctionDefinition")))
80 funcList = funcNodes.map(node => node.name)
81 }
82 }
83 return funcList;
84}
85
86/**
87 * @dev returns ABI of passed method list from passed interface
88 * @param jsonInterface Json Interface
89 * @param funcList Methods to extract the interface of
90 */
91
92function getTestFunctionsInterface (jsonInterface: FunctionDescription[], funcList: string[]): FunctionDescription[] {
93 const functionsInterface: FunctionDescription[] = []
94 const specialFunctions: string[] = ['beforeAll', 'beforeEach', 'afterAll', 'afterEach']
95 for(const func of funcList){
96 if(!specialFunctions.includes(func)) {
97 const funcInterface: FunctionDescription | undefined = jsonInterface.find(node => node.type === 'function' && node.name === func)
98 if(funcInterface) functionsInterface.push(funcInterface)
99 }
100 }
101 return functionsInterface
102}
103
104/**
105 * @dev Prepare a list of tests to run using test contract file ABI, AST & contract name
106 * @param jsonInterface File JSON interface
107 * @param fileAST File AST
108 * @param testContractName Test contract name
109 */
110
111function createRunList (jsonInterface: FunctionDescription[], fileAST: AstNode, testContractName: string): RunListInterface[] {
112 const availableFunctions: string[] = getAvailableFunctions(fileAST, testContractName)
113 const testFunctionsInterface: FunctionDescription[] = getTestFunctionsInterface(jsonInterface, availableFunctions)
114
115 let runList: RunListInterface[] = []
116
117 if (availableFunctions.indexOf('beforeAll') >= 0) {
118 runList.push({ name: 'beforeAll', type: 'internal', constant: false, payable: false })
119 }
120
121 for (const func of testFunctionsInterface) {
122 if (availableFunctions.indexOf('beforeEach') >= 0) {
123 runList.push({ name: 'beforeEach', type: 'internal', constant: false, payable: false })
124 }
125 if(func.name) runList.push({ name: func.name, signature: func.signature, type: 'test', constant: isConstant(func), payable: isPayable(func) })
126 if (availableFunctions.indexOf('afterEach') >= 0) {
127 runList.push({ name: 'afterEach', type: 'internal', constant: false, payable: false })
128 }
129 }
130
131 if (availableFunctions.indexOf('afterAll') >= 0) {
132 runList.push({ name: 'afterAll', type: 'internal', constant: false, payable: false })
133 }
134
135 return runList
136}
137
138export function runTest (testName: string, testObject: any, contractDetails: CompiledContract, fileAST: AstNode, opts: Options, testCallback: TestCbInterface, resultsCallback: ResultCbInterface): void {
139 let passingNum: number = 0
140 let failureNum: number = 0
141 let timePassed: number = 0
142 const isJSONInterfaceAvailable = testObject && testObject.options && testObject.options.jsonInterface
143 if(!isJSONInterfaceAvailable)
144 return resultsCallback(new Error('Contract interface not available'), { passingNum, failureNum, timePassed })
145
146 const runList: RunListInterface[] = createRunList(testObject.options.jsonInterface, fileAST, testName)
147 const web3 = new Web3()
148 const accts: TestResultInterface = {
149 type: 'accountList',
150 value: opts.accounts
151 }
152
153 testCallback(undefined, accts)
154
155 const resp: TestResultInterface = {
156 type: 'contract',
157 value: testName,
158 filename: testObject.filename
159 }
160
161 testCallback(undefined, resp)
162 async.eachOfLimit(runList, 1, function (func, index, next) {
163 let sender: string | null = null
164 if (func.signature) {
165 sender = getOverridedSender(contractDetails.userdoc, func.signature, contractDetails.evm.methodIdentifiers)
166 if (opts.accounts && sender) {
167 sender = opts.accounts[sender]
168 }
169 }
170 let sendParams: Record<string, string> | null = null
171 if (sender) sendParams = { from: sender }
172 const method = testObject.methods[func.name].apply(testObject.methods[func.name], [])
173 const startTime = Date.now()
174 if (func.constant) {
175 method.call(sendParams).then((result) => {
176 const time = (Date.now() - startTime) / 1000.0
177 if (result) {
178 const resp: TestResultInterface = {
179 type: 'testPass',
180 value: changeCase.sentenceCase(func.name),
181 time: time,
182 context: testName
183 }
184 testCallback(undefined, resp)
185 passingNum += 1
186 timePassed += time
187 } else {
188 const resp: TestResultInterface = {
189 type: 'testFailure',
190 value: changeCase.sentenceCase(func.name),
191 time: time,
192 errMsg: 'function returned false',
193 context: testName
194 }
195 testCallback(undefined, resp)
196 failureNum += 1
197 }
198 next()
199 })
200 } else {
201 if(func.payable) {
202 const value = getProvidedValue(contractDetails.userdoc, func.signature, contractDetails.evm.methodIdentifiers)
203 if(value) {
204 if(sendParams) sendParams.value = value
205 else sendParams = { value }
206 }
207 }
208 method.send(sendParams).on('receipt', (receipt) => {
209 try {
210 const time: number = (Date.now() - startTime) / 1000.0
211 const topic = Web3.utils.sha3('AssertionEvent(bool,string)')
212 let testPassed: boolean = false
213
214 for (const i in receipt.events) {
215 const event = receipt.events[i]
216 if (event.raw.topics.indexOf(topic) >= 0) {
217 const testEvent = web3.eth.abi.decodeParameters(['bool', 'string'], event.raw.data)
218 if (!testEvent[0]) {
219 const resp: TestResultInterface = {
220 type: 'testFailure',
221 value: changeCase.sentenceCase(func.name),
222 time: time,
223 errMsg: testEvent[1],
224 context: testName
225 };
226 testCallback(undefined, resp)
227 failureNum += 1
228 return next()
229 }
230 testPassed = true
231 }
232 }
233
234 if (testPassed) {
235 const resp: TestResultInterface = {
236 type: 'testPass',
237 value: changeCase.sentenceCase(func.name),
238 time: time,
239 context: testName
240 }
241 testCallback(undefined, resp)
242 passingNum += 1
243 }
244
245 return next()
246 } catch (err) {
247 console.error(err)
248 return next(err)
249 }
250 }).on('error', function (err: Error) {
251 console.error(err)
252 const time: number = (Date.now() - startTime) / 1000.0
253 const resp: TestResultInterface = {
254 type: 'testFailure',
255 value: changeCase.sentenceCase(func.name),
256 time: time,
257 errMsg: err.message,
258 context: testName
259 };
260 testCallback(undefined, resp)
261 failureNum += 1
262 return next()
263 })
264 }
265 }, function(error) {
266 resultsCallback(error, { passingNum, failureNum, timePassed })
267 })
268}