1 | import async from 'async'
|
2 | import * as changeCase from 'change-case'
|
3 | import Web3 from 'web3';
|
4 | import { RunListInterface, TestCbInterface, TestResultInterface, ResultCbInterface,
|
5 | CompiledContract, AstNode, Options, FunctionDescription, UserDocumentation } from './types'
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | function 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 |
|
24 |
|
25 |
|
26 |
|
27 | function isConstant(funcABI: FunctionDescription): boolean {
|
28 | return (funcABI.constant || funcABI.stateMutability === 'view' || funcABI.stateMutability === 'pure')
|
29 | }
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | function isPayable(funcABI: FunctionDescription): boolean {
|
37 | return (funcABI.payable || funcABI.stateMutability === 'payable')
|
38 | }
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | function 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 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | function 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 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | function 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 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | function 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 |
|
111 | function 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 |
|
138 | export 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 | }
|