1/* 2 * Copyright (c) 2025 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 { ArktsConfigBuilder, BuildConfig, CompileFileInfo, MockArktsConfigBuilder, ModuleInfo } from './artkts-config'; 17import { MockPluginDriver, PluginDriver, stateName } from './plugin-driver'; 18import { isNumber } from './safe-types'; 19import { 20 canProceedToState, 21 destroyConfig, 22 destroyContext, 23 initGlobal, 24 resetConfig, 25 resetContext, 26} from './global'; 27import { insertPlugin } from './compile'; 28import { PluginExecutor, Plugins, PluginState } from '../../common/plugin-context'; 29import { TesterCache } from './cache'; 30import * as arkts from '@koalaui/libarkts'; 31import * as fs from 'fs'; 32 33type TestParams = Parameters<typeof test>; 34 35type SkipFirstParam<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : never; 36 37type PluginTestHooks = { 38 [K in PluginState | `${PluginState}:${string}`]?: SkipFirstParam<TestParams>; 39}; 40 41type TestHooks = { 42 beforeAll?: Parameters<jest.Lifecycle>; 43 beforeEach?: Parameters<jest.Lifecycle>; 44 afterEach?: Parameters<jest.Lifecycle>; 45}; 46 47export interface PluginTestContext { 48 scriptSnapshot?: string; 49 errors?: string[]; 50 warnings?: string[]; 51} 52 53export interface PluginTesterOptions { 54 stopAfter: PluginState; 55 buildConfig?: BuildConfig; 56} 57 58class PluginTester { 59 private configBuilder: ArktsConfigBuilder; 60 private pluginDriver: PluginDriver; 61 private describe: string; 62 private cache: TesterCache<PluginTestContext>; 63 64 constructor(describe: string, buildConfig?: BuildConfig) { 65 this.describe = describe; 66 this.configBuilder = new MockArktsConfigBuilder(buildConfig); 67 this.pluginDriver = new MockPluginDriver(); 68 this.cache = TesterCache.getInstance(); 69 } 70 71 private loadPluginDriver(plugins: Plugins[]): void { 72 this.pluginDriver.initPlugins(plugins); 73 } 74 75 private test( 76 key: PluginState | `${PluginState}:${string}`, 77 index: arkts.Es2pandaContextState, 78 testName: string, 79 pluginHooks: PluginTestHooks, 80 plugin?: PluginExecutor 81 ): void { 82 let cached: boolean = false; 83 const cacheKey: string = `${testName}-${key}`; 84 if (index > arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED) { 85 return; 86 } 87 if (canProceedToState(index)) { 88 arkts.proceedToState(index); 89 } 90 if (plugin) { 91 insertPlugin(this.pluginDriver, plugin, index); 92 this.captureContext(cacheKey); 93 cached = true; 94 } 95 const hook: SkipFirstParam<TestParams> | undefined = pluginHooks[key]; 96 if (!!hook) { 97 if (!cached) this.captureContext(cacheKey); 98 test(testName, hook[0]?.bind(this.cache.get(cacheKey)), hook[1]); 99 } 100 } 101 102 private captureContext(cacheKey: string): void { 103 try { 104 // TODO: add error/warning handling after plugin 105 const context: PluginTestContext = this.cache.get(cacheKey) ?? {}; 106 const script: arkts.EtsScript = arkts.EtsScript.fromContext(); 107 context.scriptSnapshot = script.dumpSrc(); 108 this.cache.set(cacheKey, context); 109 } catch (e) { 110 // Do nothing 111 } 112 } 113 114 private proceedToState( 115 state: PluginState, 116 index: arkts.Es2pandaContextState, 117 testName: string, 118 pluginHooks: PluginTestHooks, 119 plugins?: PluginExecutor[] 120 ): void { 121 if (plugins && plugins.length > 0) { 122 plugins.forEach((plugin) => { 123 const pluginName: string = plugin.name; 124 const key: `${PluginState}:${string}` = `${state}:${pluginName}`; 125 this.test(key, index, `[${key}] ${testName}`, pluginHooks, plugin); 126 }); 127 } 128 this.test(state, index, `[${state}] ${testName}`, pluginHooks); 129 } 130 131 private singleFileCompile( 132 fileInfo: CompileFileInfo, 133 moduleInfo: ModuleInfo, 134 testName: string, 135 pluginHooks: PluginTestHooks, 136 stopAfter: PluginState 137 ): void { 138 let shouldStop: boolean = false; 139 140 Object.values(arkts.Es2pandaContextState) 141 .filter(isNumber) 142 .forEach((it) => { 143 if (shouldStop) { 144 return; 145 } 146 const state: PluginState = stateName(it); 147 const plugins: PluginExecutor[] | undefined = this.pluginDriver.getSortedPlugins(it); 148 this.proceedToState( 149 state, 150 it, 151 `${moduleInfo.packageName} - ${fileInfo.fileName}: ${testName}`, 152 pluginHooks, 153 plugins 154 ); 155 shouldStop = state === stopAfter; 156 }); 157 } 158 159 private traverseFile(testName: string, pluginHooks: PluginTestHooks, stopAfter: PluginState): void { 160 let once: boolean = false; 161 this.configBuilder.moduleInfos.forEach((moduleInfo) => { 162 moduleInfo.compileFileInfos.forEach((fileInfo) => { 163 if (!once) { 164 initGlobal(fileInfo, this.configBuilder.isDebug); 165 once = true; 166 } else { 167 const source: string = fs.readFileSync(fileInfo.filePath).toString(); 168 resetContext(source); 169 } 170 this.singleFileCompile(fileInfo, moduleInfo, testName, pluginHooks, stopAfter); 171 }); 172 }); 173 } 174 175 run( 176 testName: string, 177 plugins: Plugins[], 178 pluginHooks: PluginTestHooks, 179 options: PluginTesterOptions, 180 testHooks?: TestHooks 181 ): void { 182 if (!!options.buildConfig) { 183 this.configBuilder = new MockArktsConfigBuilder(options.buildConfig); 184 } 185 186 this.cache.clear(); 187 this.loadPluginDriver(plugins); 188 189 const that = this; 190 describe(this.describe, () => { 191 if (testHooks?.beforeAll) { 192 beforeAll(...testHooks.beforeAll); 193 } 194 if (testHooks?.beforeEach) { 195 beforeEach(...testHooks.beforeEach); 196 } 197 if (testHooks?.afterEach) { 198 afterEach(...testHooks.afterEach); 199 } 200 afterAll(() => { 201 destroyContext(); 202 destroyConfig(); 203 }); 204 that.traverseFile(testName, pluginHooks, options.stopAfter); 205 }); 206 } 207} 208 209export { PluginTester }; 210