1/* 2 * Copyright (c) 2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import ExtendInterface from "./ExtendInterface.js"; 17import VerificationMode from "./VerificationMode.js"; 18import { ArgumentMatchers } from "./ArgumentMatchers.js"; 19 20interface IFunction extends Function { 21 container: any; 22 original: any; 23 propName: string; 24 originalFromPrototype: boolean 25 mocker: MockKit 26} 27 28class MockKit { 29 30 private mFunctions:Array<any> = []; 31 private stubs = new Map(); 32 private recordCalls = new Map(); 33 private currentSetKey = new Map(); 34 private mockObj = null; 35 private recordMockedMethod = new Map(); 36 private originalMethod: any; 37 38 constructor() { 39 this.mFunctions = []; 40 this.stubs = new Map(); 41 this.recordCalls = new Map(); 42 this.currentSetKey = new Map(); 43 this.mockObj = null; 44 this.recordMockedMethod = new Map(); 45 } 46 47 init() { 48 this.reset(); 49 } 50 51 reset() { 52 this.mFunctions = []; 53 this.stubs = new Map() 54 this.recordCalls = new Map(); 55 this.currentSetKey = new Map(); 56 this.mockObj = null; 57 this.recordMockedMethod = new Map(); 58 } 59 60 clearAll() { 61 this.reset(); 62 } 63 64 clear(obj: any) { 65 if (!obj) throw Error("Please enter an object to be cleaned"); 66 if (typeof (obj) != 'object') throw new Error('Not a object'); 67 this.recordMockedMethod.forEach(function (value, key, map) { 68 if (key) { 69 obj[key] = value; 70 } 71 }); 72 } 73 74 ignoreMock(obj:any, method: any) { 75 if (typeof (obj) != 'object') throw new Error('Not a object'); 76 if (typeof (method) != 'function') throw new Error('Not a function'); 77 let og = this.recordMockedMethod.get(method.propName); 78 if (og) { 79 obj[method.propName] = og; 80 this.recordMockedMethod.set(method.propName, undefined); 81 } 82 } 83 84 extend(dest: any, source:any) { 85 dest["stub"] = source["stub"]; 86 dest["afterReturn"] = source["afterReturn"]; 87 dest["afterReturnNothing"] = source["afterReturnNothing"]; 88 dest["afterAction"] = source["afterAction"]; 89 dest["afterThrow"] = source["afterThrow"]; 90 dest["stubMockedCall"] = source["stubMockedCall"]; 91 dest["clear"] = source["clear"]; 92 return dest; 93 } 94 95 stubApply(f: any, params:any, returnInfo:any) { 96 let values = this.stubs.get(f); 97 if (!values) { 98 values = new Map(); 99 } 100 let key = params[0]; 101 if (typeof key == "undefined") { 102 key = "anonymous-mock-" + f.propName; 103 } 104 let matcher = new ArgumentMatchers(); 105 if (matcher.matcheStubKey(key)) { 106 key = matcher.matcheStubKey(key); 107 if (key) { 108 this.currentSetKey.set(f, key); 109 } 110 } 111 values.set(key, returnInfo); 112 this.stubs.set(f, values); 113 } 114 115 getReturnInfo(f: any, params:any) { 116 let values = this.stubs.get(f); 117 if (!values) { 118 return undefined; 119 } 120 let retrunKet = params[0]; 121 if (typeof retrunKet == "undefined") { 122 retrunKet = "anonymous-mock-" + f.propName; 123 } 124 let stubSetKey = this.currentSetKey.get(f); 125 126 if (stubSetKey && (typeof (retrunKet) != "undefined")) { 127 retrunKet = stubSetKey; 128 } 129 let matcher = new ArgumentMatchers(); 130 if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) != stubSetKey) { 131 retrunKet = params[0]; 132 } 133 134 values.forEach(function (value: any, key: any, map: any) { 135 if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) { 136 retrunKet = key; 137 } 138 }); 139 140 return values.get(retrunKet); 141 } 142 143 findName(obj: any, value: any) { 144 let properties = this.findProperties(obj); 145 let name = ''; 146 properties.filter((item:any) => (item !== 'caller' && item !== 'arguments')).forEach( 147 function (va1:any, idx:any, array:any) { 148 if (obj[va1] === value) { 149 name = va1; 150 } 151 } 152 ); 153 return name; 154 } 155 156 isFunctionFromPrototype(f: Function, container:Function, propName: string) { 157 if (container.constructor != Object && container.constructor.prototype !== container) { 158 return container.constructor.prototype[propName] === f; 159 } 160 return false; 161 } 162 163 findProperties(obj: any, ...arg: Array<any>) { 164 function getProperty(new_obj:any): Array<any> { 165 if (new_obj.__proto__ === null) { 166 return []; 167 } 168 let properties = Object.getOwnPropertyNames(new_obj); 169 return [...properties, ...getProperty(new_obj.__proto__)]; 170 } 171 return getProperty(obj); 172 } 173 174 recordMethodCall(originalMethod: any, args: any) { 175 originalMethod['getName'] = function () { 176 return this.name || this.toString().match(/function\s*([^(]*)\(/)[1]; 177 } 178 let name = originalMethod.getName(); 179 let arglistString = name + '(' + Array.from(args).toString() + ')'; 180 let records = this.recordCalls.get(arglistString); 181 if (!records) { 182 records = 0; 183 } 184 records++; 185 this.recordCalls.set(arglistString, records); 186 } 187 188 mockFunc(originalObject:any, originalMethod:any) { 189 let tmp = this; 190 this.originalMethod = originalMethod; 191 const _this = this; 192 let f:any = function () { 193 let args = arguments; 194 let action = tmp.getReturnInfo(f, args); 195 if (originalMethod) { 196 tmp.recordMethodCall(originalMethod, args); 197 } 198 if (action) { 199 return <IFunction> action.apply(_this, args); 200 } 201 }; 202 203 f.container = null || originalObject; 204 f.original = originalMethod || null; 205 206 if (originalObject && originalMethod) { 207 if (typeof (originalMethod) != 'function') throw new Error('Not a function'); 208 var name = this.findName(originalObject, originalMethod); 209 originalObject[name] = f; 210 this.recordMockedMethod.set(name, originalMethod); 211 f.propName = name; 212 f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName); 213 } 214 f.mocker = this; 215 this.mFunctions.push(f); 216 this.extend(f, new ExtendInterface(this)); 217 return f; 218 } 219 220 verify(methodName:any, argsArray:any) { 221 if (!methodName) { 222 throw Error("not a function name"); 223 } 224 let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')'); 225 return new VerificationMode(a ? a : 0); 226 } 227 228 mockObject(object: any) { 229 if (!object || typeof object === "string") { 230 throw Error(`this ${object} cannot be mocked`); 231 } 232 const _this = this; 233 let mockedObject:any = {}; 234 let keys = Reflect.ownKeys(object); 235 keys.filter(key => (typeof Reflect.get(object, key)) === 'function') 236 .forEach((key:any) => { 237 mockedObject[key] = object[key]; 238 mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]); 239 }); 240 return mockedObject; 241 } 242} 243 244function ifMockedFunction(f: any) { 245 if (Object.prototype.toString.call(f) != "[object Function]" && 246 Object.prototype.toString.call(f) != "[object AsyncFunction]") { 247 throw Error("not a function"); 248 } 249 if (!f.stub) { 250 throw Error("not a mock function"); 251 } 252 return true; 253} 254 255function when(f: any) { 256 if (ifMockedFunction(f)) { 257 return f.stub.bind(f); 258 } 259} 260 261function MockSetup(target: Object, propertyName: string | Symbol, descriptor: TypedPropertyDescriptor<() => void>): void { 262 const aboutToAppearOrigin = target.aboutToAppear; 263 const setup = descriptor.value; 264 target.aboutToAppear = function (...args: any[]) { 265 if (target.__Param) { // copy attributes and params of the original context 266 try { 267 const map = target.__Param as Map<string, unknown>; 268 for (const [key, val] of map) { 269 this[key] = val; // 'this' refers to context of current function 270 } 271 } catch (e) { 272 throw new Error(`Mock setup param error: ${e}`); 273 } 274 } 275 276 if (setup) { // apply the mock content 277 try { 278 setup.apply(this); 279 } catch (e) { 280 throw new Error(`Mock setup apply error: ${e}`); 281 } 282 } 283 284 if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context 285 aboutToAppearOrigin.apply(this, args); 286 } 287 } 288} 289 290export { 291 MockSetup, 292 MockKit, 293 when 294};