2
* Copyright (c) 2024 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
7
* http://www.apache.org/licenses/LICENSE-2.0
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.
15
import * as ts from "typescript"
16
import { asString, nameOrNullForIdl as nameOrUndefined, getDeclarationsByNode } from "./util"
17
import { GenericVisitor } from "./options"
18
import {randInt, randString, pick, pickArray} from "./rand_utils";
20
const LAMBDA = "LAMBDA"
22
export class TestGeneratorVisitor implements GenericVisitor<string[]> {
23
private interfacesToTest = new Set<string>()
24
private methodsToTest = new Set<string>()
25
private propertiesToTest = new Set<string>()
27
constructor(private sourceFile: ts.SourceFile, private typeChecker: ts.TypeChecker,
28
interfacesToTest: string| undefined,
29
methodsToTest: string| undefined,
30
propertiesToTest: string| undefined) {
31
interfacesToTest?.split(",")?.map(it => this.interfacesToTest.add(`${it}Attribute`))
32
methodsToTest?.split(",")?.map(it => this.methodsToTest.add(it))
33
propertiesToTest?.split(",")?.map(it => this.propertiesToTest.add(it))
36
private output: string[] = []
38
visitWholeFile(): string[] {
39
ts.forEachChild(this.sourceFile, (node) => this.visit(node))
43
visit(node: ts.Node) {
44
if (ts.isClassDeclaration(node)) {
46
} else if (ts.isInterfaceDeclaration(node)) {
47
this.testInterface(node)
48
} else if (ts.isModuleDeclaration(node)) {
49
// This is a namespace, visit its children
50
ts.forEachChild(node, (node) => this.visit(node));
54
testClass(node: ts.ClassDeclaration) {
55
if (this.interfacesToTest.size > 0 && !this.interfacesToTest.has(nameOrUndefined(node.name)!)) return
56
this.prologue(node.name!)
57
node.members.forEach(child => {
58
if (ts.isConstructorDeclaration(child)) {
59
this.testConstructor(child)
60
} else if (ts.isMethodDeclaration(child)) {
61
this.testMethod(child)
62
} else if (ts.isPropertyDeclaration(child)) {
63
this.testProperty(child)
66
this.epilogue(node.name!)
69
testInterface(node: ts.InterfaceDeclaration) {
70
if (this.interfacesToTest.size > 0 && !this.interfacesToTest.has(nameOrUndefined(node.name)!)) return
71
this.prologue(node.name!)
72
let members = this.membersWithFakeOverrides(node)
73
members.forEach(child => {
74
if (ts.isConstructSignatureDeclaration(child)) {
75
this.testConstructor(child)
76
} else if (ts.isMethodSignature(child)) {
77
this.testMethod(child)
78
} else if (ts.isPropertySignature(child)) {
79
this.testProperty(child)
82
this.epilogue(node.name!)
85
testConstructor(ctor: ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration) {
86
if (this.methodsToTest.size > 0 && !this.methodsToTest.has("constructor")) return
89
testMethod(method: ts.MethodDeclaration | ts.MethodSignature) {
90
if (this.methodsToTest.size > 0 && !this.methodsToTest.has(nameOrUndefined(method.name)!)) return
92
this.generateArgs(method).forEach(args => {
93
let methodName = nameOrUndefined(method.name)
96
let passedArgs = args.replaceAll(LAMBDA, `() => {}`)
97
let expectedArgs = args.replaceAll(LAMBDA, `"Function 42"`)
99
let golden = `${methodName}(${expectedArgs})`
100
this.output.push(` checkResult("${methodName}", () => peer.${methodName}Attribute(${passedArgs}), \`${golden}\`)`)
104
generateArgs(method: ts.MethodDeclaration | ts.MethodSignature): string[] {
105
let args = method.parameters.map(it => this.generateArg(it))
106
if (args.find(it => it === undefined)) {
107
console.log("Cannot map some argument")
110
return pick(method.parameters.map(it => it), key => this.generateArg(key))
113
generateArg(param: ts.ParameterDeclaration): string[] {
114
return this.generateValueOfType(param.type!)
117
generateValueOfType(type: ts.TypeNode): string[] {
118
if (type.kind == ts.SyntaxKind.UndefinedKeyword) {
121
if (type.kind == ts.SyntaxKind.NullKeyword) {
124
if (type.kind == ts.SyntaxKind.NumberKeyword) {
125
return [`0`, `-1`, `${randInt(2048, -1024)}`, `-0.59`, `93.54`]
127
if (type.kind == ts.SyntaxKind.StringKeyword) {
128
return ['""',`"${randString(randInt(16))}"`]
130
if (type.kind == ts.SyntaxKind.BooleanKeyword) {
131
return ["false", "true"]
133
if (ts.isTypeReferenceNode(type)) {
134
let name = type.typeName
135
if (!ts.isIdentifier(name)) {
136
console.log(`${asString(name)} is not identifier`)
139
let decls = getDeclarationsByNode(this.typeChecker, name)
142
if (decl && ts.isEnumDeclaration(decl)) {
143
// TBD: Use enum constants
144
// let name = decl.name
145
// ${nameOrUndefined(name)}.${nameOrUndefined(it.name)!}
146
return decl.members.map((it, index) => `${index}`)
148
if (decl && ts.isTypeAliasDeclaration(decl)) {
149
return this.generateValueOfType(decl.type)
151
if (decl && ts.isInterfaceDeclaration(decl)) {
153
let interfaceName = asString(name)
154
// Array from built-in
155
if (interfaceName === "Array") {
156
return type.typeArguments ? pickArray(this.generateValueOfType(type.typeArguments[0])) : []
158
// Optional from stdlib.d.ts
159
if (interfaceName === "Optional") {
160
if (type.typeArguments) {
161
let argType = type.typeArguments[0]
162
if (ts.isTypeNode(argType)) {
163
return [`undefined`, ...this.generateValueOfType(argType)]
169
return pick(decl.members.filter(ts.isPropertySignature), (key) =>
170
this.generateValueOfType(key.type!)
171
.map(it => `${nameOrUndefined(key.name)}: ${it}`))
172
.map(it => `{${it}}`)
174
if (decl && ts.isClassDeclaration(decl)) {
176
let className = nameOrUndefined(decl.name)
177
console.log(`class: ${nameOrUndefined(decl.name)}`)
178
decl.members.forEach(it => console.log(`class member: ${nameOrUndefined(it.name)}`))
180
let consturctors = decl.members.filter(ts.isConstructorDeclaration)
181
if (consturctors.length > 0) {
182
let constructor = consturctors[randInt(consturctors.length)]
183
constructor.parameters.forEach(it => {console.log(`constructor param: ${nameOrUndefined(it.name)}`)})
185
// TBD: add imports for classes with constructors
187
return pick(constructor.parameters.map (it => it), (key) =>
188
this.generateValueOfType(key.type!)
189
.map(it => `${it}`)) // TBD: Use generated class
190
// .map(it => `${nameOrUndefined(key.name)}: ${it}`))
191
.map(it => `new ${className}(${it})`) // TBD: Use generated class
192
// .map(it => `{${it}}`)
198
return pick(decl.members.filter(ts.isPropertyDeclaration), (key) =>
199
this.generateValueOfType(key.type!)
200
.map(it => `${nameOrUndefined(key.name)}: ${it}`))
201
.map(it => `{${it}}`)
203
console.log(`Cannot create value of type ${asString(type)}`)
207
if (ts.isOptionalTypeNode(type)) {
208
return [`undefined`, ...this.generateValueOfType(type.type)]
210
if (ts.isUnionTypeNode(type)) {
211
return type.types.flatMap(it => this.generateValueOfType(it))
213
if (ts.isArrayTypeNode(type)) {
214
return pickArray(this.generateValueOfType(type.elementType))
216
if (ts.isLiteralTypeNode(type)) {
217
let literal = type.literal
218
if (ts.isStringLiteral(literal)) return [`${literal.getText(this.sourceFile)}`]
219
console.log(`Cannot create value of literal type ${asString(literal)}`)
222
if (ts.isTupleTypeNode(type)) {
223
// return [`[${type.elements.map(it => this.generateValueOfType(it)).join(",")}]`]
224
return pick(type.elements.map(it => it), (key) =>
225
this.generateValueOfType(key))
226
.map(it => `[${it}]`)
228
if (ts.isFunctionTypeNode(type)) {
229
// TODO: be smarter here
232
if (ts.isTypeLiteralNode(type)) {
233
// TODO: be smarter here
234
return pick(type.members.filter(ts.isPropertySignature), (key) =>
235
this.generateValueOfType(key.type!).map(it => `${nameOrUndefined(key.name)}: ${it}`))
236
.map(it => `{${it}}`)
238
console.log(`Cannot create value of type ${asString(type)}`)
242
testProperty(property: ts.PropertyDeclaration | ts.PropertySignature) {
243
if (this.methodsToTest.size > 0 && !this.methodsToTest.has(nameOrUndefined(property.name)!)) return
244
console.log(`test prop ${nameOrUndefined(property.name)!}`)
247
getClassName(name: ts.Identifier) : string {
248
const clazzName = nameOrUndefined(name)!
249
return clazzName.endsWith("Attribute") ? clazzName.replace("Attribute", "") : clazzName
252
prologue(name: ts.Identifier) {
253
let clazzName = this.getClassName(name)!
254
this.output.push(`import { Ark${clazzName}Peer } from "@arkoala/arkui/Ark${clazzName}Peer"`)
256
this.output.push(`function check${clazzName}() {`)
257
this.output.push(` let peer = new Ark${clazzName}Peer(ArkUINodeType.${clazzName})`)
260
epilogue(name: ts.Identifier) {
261
let clazzName = this.getClassName(name)!
262
this.output.push(`}`)
263
this.output.push(`check${clazzName}()`)
264
this.output.push(`\n`)
267
membersWithFakeOverrides(node: ts.InterfaceDeclaration): ts.TypeElement[] {
268
const result: ts.TypeElement[] = []
269
const worklist: ts.InterfaceDeclaration[] = [node]
270
const overridden = new Set<string>()
271
while (worklist.length != 0) {
272
const next = worklist.shift()!
273
const fakeOverrides = this.filterNotOverridden(overridden, next)
275
.map(it => nameOrUndefined(it.name))
276
.forEach(it => it ? overridden.add(it) : undefined)
277
result.push(...fakeOverrides)
278
const bases = next.heritageClauses
279
?.flatMap(it => this.baseDeclarations(it))
280
?.filter(it => ts.isInterfaceDeclaration(it)) as ts.InterfaceDeclaration[]
282
worklist.push(...bases)
287
filterNotOverridden(overridden: Set<string>, node: ts.InterfaceDeclaration): ts.TypeElement[] {
288
return node.members.filter(it =>
289
it.name && ts.isIdentifier(it.name) && !overridden.has(ts.idText(it.name))
293
baseDeclarations(heritage: ts.HeritageClause): ts.Declaration[] {
294
return this.heritageIdentifiers(heritage)
295
.map(it => getDeclarationsByNode(this.typeChecker, it)[0])
299
heritageIdentifiers(heritage: ts.HeritageClause): ts.Identifier[] {
300
return heritage.types.map(it => {
301
return ts.isIdentifier(it.expression) ? it.expression : undefined
302
}).filter(it => !!it) as ts.Identifier[]