diff --git a/.eslintrc.json b/.eslintrc.json index 8ec8d47dd..91d1fe1f7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -60,6 +60,8 @@ } ], "unicorn/filename-case": "off", + "unicorn/prefer-ternary": "warn", + // "no-unused-vars": "off", // "@typescript-eslint/no-unused-vars": "off", // "unused-imports/no-unused-imports": "error", diff --git a/libraries/analysis-javascript/index.ts b/libraries/analysis-javascript/index.ts index 0dcfc0d69..671022577 100644 --- a/libraries/analysis-javascript/index.ts +++ b/libraries/analysis-javascript/index.ts @@ -56,7 +56,6 @@ export * from "./lib/type/resolving/TypeEnum"; export * from "./lib/type/resolving/TypeModel"; export * from "./lib/type/resolving/TypeModelFactory"; export * from "./lib/type/resolving/InferenceTypeModelFactory"; -export * from "./lib/type/resolving/RandomTypeModelFactory"; export * from "./lib/utils/fileSystem"; diff --git a/libraries/analysis-javascript/lib/constant/ConstantPool.ts b/libraries/analysis-javascript/lib/constant/ConstantPool.ts index 1e7098093..ac53119de 100644 --- a/libraries/analysis-javascript/lib/constant/ConstantPool.ts +++ b/libraries/analysis-javascript/lib/constant/ConstantPool.ts @@ -32,9 +32,6 @@ export class ConstantPool { this._numericPool = new Map(); this.addNumeric(Math.PI); this.addNumeric(Math.E); - this.addNumeric(-1); - this.addNumeric(0); - this.addNumeric(+1); this._integerPool = new Map(); this.addInteger(-1); @@ -63,6 +60,7 @@ export class ConstantPool { this._integerPool.set(value, 1); } this._integerCount++; + this.addNumeric(value); } public addBigInt(value: bigint): void { diff --git a/libraries/analysis-javascript/lib/target/Target.ts b/libraries/analysis-javascript/lib/target/Target.ts index 6403a1e63..177560f75 100644 --- a/libraries/analysis-javascript/lib/target/Target.ts +++ b/libraries/analysis-javascript/lib/target/Target.ts @@ -36,6 +36,7 @@ export interface SubTarget extends CoreSubTarget { export interface NamedSubTarget extends SubTarget { name: string; + typeId: string; } export type Exportable = { diff --git a/libraries/analysis-javascript/lib/target/TargetVisitor.ts b/libraries/analysis-javascript/lib/target/TargetVisitor.ts index 248ca41b8..d1603b326 100644 --- a/libraries/analysis-javascript/lib/target/TargetVisitor.ts +++ b/libraries/analysis-javascript/lib/target/TargetVisitor.ts @@ -22,14 +22,16 @@ import { TargetType } from "@syntest/analysis"; import { AbstractSyntaxTreeVisitor } from "@syntest/ast-visitor-javascript"; import { + Callable, ClassTarget, + Exportable, FunctionTarget, MethodTarget, + NamedSubTarget, ObjectFunctionTarget, ObjectTarget, SubTarget, } from "./Target"; -import { VisibilityType } from "./VisibilityType"; import { Export } from "./export/Export"; import { unsupportedSyntax } from "../utils/diagnostics"; import { getLogger, Logger } from "@syntest/logging"; @@ -174,6 +176,18 @@ export class TargetVisitor extends AbstractSyntaxTreeVisitor { // e.g. x.y = class {} // e.g. x.y = function {} // e.g. x.y = () => {} + if ( + assigned.property.name === "exports" && + assigned.object.type === "Identifier" && + assigned.object.name === "module" + ) { + // e.g. module.exports = class {} + // e.g. module.exports = function {} + // e.g. module.exports = () => {} + return "id" in parentNode.right + ? parentNode.right.id.name + : "anonymousFunction"; + } return assigned.property.name; } else { // e.g. x.? = class {} @@ -260,18 +274,14 @@ export class TargetVisitor extends AbstractSyntaxTreeVisitor { // e.g. () => {} // Should not be possible throw new Error( - `unknown class expression ${parentNode.type} in ${this.filePath}` + `unknown class expression ${parentNode.type} in ${this._getNodeId( + path + )}` ); } } } - public FunctionExpression: (path: NodePath) => void = ( - path - ) => { - this._functionExpression(path); - }; - public FunctionDeclaration: (path: NodePath) => void = (path) => { // e.g. function x() {} @@ -279,446 +289,549 @@ export class TargetVisitor extends AbstractSyntaxTreeVisitor { const id = this._getNodeId(path); const export_ = this._getExport(id); - const target: FunctionTarget = { - id: id, - name: targetName, - type: TargetType.FUNCTION, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - isAsync: path.node.async, - }; - - this.subTargets.push(target); + this._extractFromFunction( + path, + id, + id, + targetName, + export_, + false, + false + ); + + path.skip(); }; - public ClassExpression: (path: NodePath) => void = ( + public ClassDeclaration: (path: NodePath) => void = ( path ) => { - const targetName = this._getTargetNameOfExpression(path); - const export_ = this._getExport(this._getNodeId(path)); + // e.g. class A {} + const targetName = this._getTargetNameOfDeclaration(path); + const id = this._getNodeId(path); + const export_ = this._getExport(id); - const target: ClassTarget = { - id: `${this._getNodeId(path)}`, - name: targetName, - type: TargetType.CLASS, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - }; + this._extractFromClass(path, id, id, targetName, export_); - this.subTargets.push(target); + path.skip(); }; - public ClassDeclaration: (path: NodePath) => void = ( + public FunctionExpression: (path: NodePath) => void = ( path ) => { - // e.g. class A {} - const targetName = this._getTargetNameOfDeclaration(path); - const export_ = this._getExport(this._getNodeId(path)); + // only thing left where these can be found is: + // call(function () {}) + const targetName = this._getTargetNameOfExpression(path); + const id = this._getNodeId(path); + const export_ = this._getExport(id); - const target: ClassTarget = { - id: `${this._getNodeId(path)}`, - name: targetName, - type: TargetType.CLASS, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - }; + this._extractFromFunction(path, id, id, targetName, export_, false, false); - this.subTargets.push(target); + path.skip(); }; - private _getParentClassId( - path: NodePath< - | t.ClassMethod - | t.ClassProperty - | t.ClassPrivateMethod - | t.ClassPrivateProperty - > - ): string { - return this._getNodeId(path.parentPath.parentPath); - } + public ClassExpression: (path: NodePath) => void = ( + path + ) => { + // only thing left where these can be found is: + // call(class {}) + const targetName = this._getTargetNameOfExpression(path); + const id = this._getNodeId(path); + const export_ = this._getExport(id); - public ClassMethod: (path: NodePath) => void = (path) => { - if (path.parentPath.type !== "ClassBody") { - // unsupported - // not possible i think - throw new Error("unknown class method parent"); - } + this._extractFromClass(path, id, id, targetName, export_); - const parentClassId: string = this._getParentClassId(path); + path.skip(); + }; - if (path.node.key.type !== "Identifier") { - // e.g. class A { ?() {} } - // unsupported - // not possible i think - throw new Error("unknown class method key"); - } + public ArrowFunctionExpression: ( + path: NodePath + ) => void = (path) => { + // only thing left where these can be found is: + // call(() => {}) + const targetName = this._getTargetNameOfExpression(path); + const id = this._getNodeId(path); + const export_ = this._getExport(id); - const targetName = path.node.key.name; + this._extractFromFunction(path, id, id, targetName, export_, false, false); - let visibility = VisibilityType.PUBLIC; - if (path.node.access === "private") { - visibility = VisibilityType.PRIVATE; - } else if (path.node.access === "protected") { - visibility = VisibilityType.PROTECTED; - } + path.skip(); + }; - const target: MethodTarget = { - id: `${this._getNodeId(path)}`, - name: targetName, - type: TargetType.METHOD, - classId: parentClassId, - isStatic: path.node.static, - isAsync: path.node.async, - methodType: path.node.kind, - visibility: visibility, - }; + public VariableDeclarator: (path: NodePath) => void = ( + path + ) => { + if (!path.has("init")) { + path.skip(); + return; + } + const idPath = >path.get("id"); + const init = path.get("init"); + + const targetName = idPath.node.name; + const id = this._getNodeId(path); + const typeId = this._getNodeId(init); + const export_ = this._getExport(id); + + if (init.isFunction()) { + this._extractFromFunction( + init, + id, + typeId, + targetName, + export_, + false, + false + ); + } else if (init.isClass()) { + this._extractFromClass(init, id, typeId, targetName, export_); + } else if (init.isObjectExpression()) { + this._extractFromObjectExpression(init, id, typeId, targetName, export_); + } else { + // TODO + } - this.subTargets.push(target); + path.skip(); }; - private _functionExpression( - path: NodePath - ) { - // e.g. const x = () => {} - const targetName = this._getTargetNameOfExpression(path); + public AssignmentExpression: ( + path: NodePath + ) => void = (path) => { + const left = path.get("left"); + const right = path.get("right"); - const parent = path.parentPath; - let left; if ( - parent.isAssignmentExpression() && - ((left = parent.get("left")), left.isMemberExpression()) + !right.isFunction() && + !right.isClass() && + !right.isObjectExpression() ) { - let object = left.get("object"); - const property = left.get("property"); - - if (object.isMemberExpression()) { - const subObject = object.get("object"); - const subProperty = object.get("property"); - - if (!subProperty.isIdentifier()) { - // e.g. a.x().y = function () {} - // unsupported - throw new Error( - unsupportedSyntax(path.node.type, this._getNodeId(path)) - ); - } - - if (subProperty.node.name === "prototype") { - // e.g. a.prototype.y = function() {} - object = subObject; - if (object.isIdentifier()) { - const prototypeName = object.node.name; - // find function - const target = ( - this._subTargets.find( - (target) => - target.type === TargetType.FUNCTION && - (target).name === prototypeName - ) - ); - if (target) { - // remove - this._subTargets = this._subTargets.filter( - (subTarget) => subTarget.id !== target.id - ); - // add new - this._subTargets.push( - { - id: target.id, - type: TargetType.CLASS, - name: target.name, - exported: target.exported, - renamedTo: target.renamedTo, - module: target.module, - default: target.default, - }, - // add constructor - { - id: target.id, - type: TargetType.METHOD, - name: "constructor", - classId: target.id, - - visibility: VisibilityType.PUBLIC, - - methodType: "constructor", - isStatic: false, - isAsync: target.isAsync, - } - ); - } - // add this as class method - if (!property.isIdentifier()) { - throw new Error( - unsupportedSyntax(path.node.type, this._getNodeId(path)) - ); - } + return; + } - this._subTargets.push({ - id: `${this._getNodeId(path)}`, - type: TargetType.METHOD, - name: property.node.name, - classId: this._getBindingId(object), + const targetName = this._getTargetNameOfExpression(right); + let isObject = false; + let isMethod = false; + let objectId: string; - visibility: VisibilityType.PUBLIC, + let id: string = this._getBindingId(left); + if (left.isMemberExpression()) { + const object = left.get("object"); + const property = left.get("property"); - methodType: "method", - isStatic: false, - isAsync: path.node.async, - }); - return; - } else { - // e.g. a().prototype.y = function() {} - throw new Error( - unsupportedSyntax(path.node.type, this._getNodeId(path)) - ); - } - } else { - // e.g. a.x.y = function () {} - // unsupported for now should create a objecttarget as a subtarget - throw new Error( - unsupportedSyntax(path.node.type, this._getNodeId(path)) - ); - } + if (left.get("property").isIdentifier() && left.node.computed) { + TargetVisitor.LOGGER.warn( + "We do not support dynamic computed properties: x[a] = ?" + ); + path.skip(); + return; + } else if (!left.get("property").isIdentifier() && !left.node.computed) { + // we also dont support a.f() = ? + // or equivalent + path.skip(); + return; } - // e.g. a.x = function () {} if (object.isIdentifier()) { - if ( - left.node.computed == true && - !property.node.type.includes("Literal") - ) { - // e.g. x[y] = class {} - // e.g. x[y] = function {} - // e.g. x[y] = () => {} - // TODO unsupported cannot get the name unless executing - TargetVisitor.LOGGER.warn( - `This tool does not support computed property assignments. Found one at ${this._getNodeId( - path - )}` - ); - return; - } - - const functionName = property.isIdentifier() - ? property.node.name - : "value" in property.node - ? property.node.value.toString() - : "null"; - + // x.? = ? + // x['?'] = ? if (object.node.name === "exports") { - // e.g. exports.x = function () {} - // this is simply a function not an object function - const export_ = this._getExport(this._getNodeId(path)); - - const functionTarget: FunctionTarget = { - id: `${this._getNodeId(path)}`, - type: TargetType.FUNCTION, - name: functionName, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - isAsync: path.node.async, - }; - this._subTargets.push(functionTarget); + // exports.? = ? + isObject = false; + id = this._getBindingId(right); } else if ( object.node.name === "module" && property.isIdentifier() && property.node.name === "exports" ) { - // e.g. module.exports = function () {} - // this is simply a function not an object function - const export_ = this._getExport(this._getNodeId(path)); - - const functionTarget: FunctionTarget = { - id: `${this._getNodeId(path)}`, - type: TargetType.FUNCTION, - name: path.has("id") - ? (>path.get("id")).node.name - : "default", - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - isAsync: path.node.async, - }; - - this._subTargets.push(functionTarget); + // module.exports = ? + isObject = false; + id = this._getBindingId(right); } else { - const export_ = this._getExport(this._getBindingId(object)); - - const objectTarget: ObjectTarget = { - type: TargetType.OBJECT, - name: object.node.name, - id: `${this._getBindingId(object)}`, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, + isObject = true; + objectId = this._getBindingId(object); + // find object + const objectTarget = this._subTargets.find( + (value) => value.id === objectId && value.type === TargetType.OBJECT + ); + + if (!objectTarget) { + const export_ = this._getExport(objectId); + // create one if it does not exist + const objectTarget: ObjectTarget = { + id: objectId, + typeId: objectId, + name: object.node.name, + type: TargetType.OBJECT, + exported: !!export_, + default: export_ ? export_.default : false, + module: export_ ? export_.module : false, + }; + this._subTargets.push(objectTarget); + } + } + } else if (object.isMemberExpression()) { + // ?.?.? = ? + const subObject = object.get("object"); + const subProperty = object.get("property"); + // what about module.exports.x + if ( + subObject.isIdentifier() && + subProperty.isIdentifier() && + subProperty.node.name === "prototype" + ) { + // x.prototype.? = ? + objectId = this._getBindingId(subObject); + const objectTarget = ( + this._subTargets.find((value) => value.id === objectId) + ); + + const newTargetClass: ClassTarget = { + id: objectTarget.id, + type: TargetType.CLASS, + name: objectTarget.name, + typeId: objectTarget.id, + exported: objectTarget.exported, + renamedTo: objectTarget.renamedTo, + module: objectTarget.module, + default: objectTarget.default, }; - const objectFunctionTarget: ObjectFunctionTarget = { - type: TargetType.OBJECT_FUNCTION, - objectId: `${this._getBindingId(object)}`, - name: functionName, - id: `${this._getNodeId(path)}`, - isAsync: path.node.async, + + // replace original target by prototype class + this._subTargets[this._subTargets.indexOf(objectTarget)] = + newTargetClass; + + const constructorTarget: MethodTarget = { + id: objectTarget.id, + type: TargetType.METHOD, + name: objectTarget.name, + typeId: objectTarget.id, + methodType: "constructor", + classId: objectTarget.id, + visibility: "public", + isStatic: false, + isAsync: + "isAsync" in objectTarget + ? (objectTarget).isAsync + : false, }; - this.subTargets.push(objectTarget, objectFunctionTarget); + this._subTargets.push(constructorTarget); + + isMethod = true; } - } else if (object.isThisExpression()) { - // TODO repair this - // get the this scope object name - // create new object function target - return; } else { - // e.g. a().x = function () {} - // unsupported + path.skip(); + return; + } + } + + const typeId = this._getNodeId(right); + const export_ = this._getExport(isObject ? objectId : id); + + if (right.isFunction()) { + this._extractFromFunction( + right, + id, + typeId, + targetName, + export_, + isObject, + isMethod, + objectId + ); + } else if (right.isClass()) { + this._extractFromClass(right, id, typeId, targetName, export_); + } else if (right.isObjectExpression()) { + this._extractFromObjectExpression(right, id, typeId, targetName, export_); + } else { + // TODO + } + + path.skip(); + }; + + private _extractFromFunction( + path: NodePath, + functionId: string, + typeId: string, + functionName: string, + export_: Export | undefined, + isObjectFunction: boolean, + isMethod: boolean, + superId?: string + ) { + let target: FunctionTarget | ObjectFunctionTarget | MethodTarget; + + if (isObjectFunction && isMethod) { + throw new Error("Cannot be method and object function"); + } + + if (isObjectFunction) { + if (!superId) { throw new Error( - unsupportedSyntax(path.node.type, this._getNodeId(path)) + "if it is an object function the object id should be given" ); } - } else - switch (parent.node.type) { - case "ClassPrivateProperty": { - // e.g. class A { #x = () => {} } - // unsupported - throw new Error("unknown class method parent"); - } - case "ClassProperty": { - // e.g. class A { x = () => {} } - const parentClassId: string = this._getParentClassId( - >path.parentPath - ); + target = { + id: functionId, + typeId: typeId, + objectId: superId, + name: functionName, + type: TargetType.OBJECT_FUNCTION, + isAsync: path.node.async, + }; + } else if (isMethod) { + if (!superId) { + throw new Error( + "if it is an object function the object id should be given" + ); + } + target = { + id: functionId, + typeId: typeId, + classId: superId, + name: functionName, + type: TargetType.METHOD, + isAsync: path.node.async, + methodType: path.isClassMethod() ? path.node.kind : "method", + visibility: + path.isClassMethod() && path.node.access + ? path.node.access + : "public", + isStatic: + path.isClassMethod() || path.isClassProperty() + ? path.node.static + : false, + }; + } else { + target = { + id: functionId, + typeId: typeId, + name: functionName, + type: TargetType.FUNCTION, + exported: !!export_, + default: export_ ? export_.default : false, + module: export_ ? export_.module : false, + isAsync: path.node.async, + }; + } - const visibility = VisibilityType.PUBLIC; - // apparantly there is no access property on class properties - // if (parentNode.access === "private") { - // visibility = VisibilityType.PRIVATE; - // } else if (parentNode.access === "protected") { - // visibility = VisibilityType.PROTECTED; - // } - - const target: MethodTarget = { - id: `${this._getNodeId(path)}`, - classId: parentClassId, - name: targetName, - type: TargetType.METHOD, - isStatic: (parent.node).static, - isAsync: path.node.async, - methodType: "method", - visibility: visibility, - }; + this._subTargets.push(target); - this.subTargets.push(target); + const body = path.get("body"); - break; + if (Array.isArray(body)) { + throw new TypeError("weird function body"); + } else { + body.visit(); + } + } + + private _extractFromObjectExpression( + path: NodePath, + objectId: string, + typeId: string, + objectName: string, + export_?: Export + ) { + const target: ObjectTarget = { + id: objectId, + typeId: typeId, + name: objectName, + type: TargetType.OBJECT, + exported: !!export_, + default: export_ ? export_.default : false, + module: export_ ? export_.module : false, + }; + + this._subTargets.push(target); + + // loop over object properties + for (const property of path.get("properties")) { + if (property.isObjectMethod()) { + if (property.node.key.type !== "Identifier") { + // e.g. class A { ?() {} } + // unsupported + // not possible i think + throw new Error("unknown class method key"); } - case "VariableDeclarator": { - if (!path.parentPath.has("id")) { - // unsupported - throw new Error( - unsupportedSyntax(path.node.type, this._getNodeId(path)) - ); + const targetName = property.node.key.name; + + const id = this._getNodeId(property); + this._extractFromFunction( + property, + id, + id, + targetName, + undefined, + true, + false, + objectId + ); + } else if (property.isObjectProperty()) { + const key = property.get("key"); + const value = property.get("value"); + + if (value) { + const id = this._getNodeId(property); + let targetName: string; + if (key.isIdentifier()) { + targetName = key.node.name; + } else if ( + key.isStringLiteral() || + key.isBooleanLiteral() || + key.isNumericLiteral() || + key.isBigIntLiteral() + ) { + targetName = `${key.node.value}`; } - const export_ = this._getExport(this._getNodeId(path.parentPath)); + if (value.isFunction()) { + this._extractFromFunction( + value, + id, + id, + targetName, + undefined, + true, + false, + objectId + ); + } else if (value.isClass()) { + this._extractFromClass(value, id, id, targetName); + } else if (value.isObjectExpression()) { + this._extractFromObjectExpression(value, id, id, targetName); + } else { + // TODO + } + } + } else if (property.isSpreadElement()) { + // TODO + // extract the spread element + } + } + } - const target: FunctionTarget = { - id: `${this._getNodeId(path.parentPath)}`, - name: targetName, - type: TargetType.FUNCTION, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - isAsync: path.node.async, - }; + private _extractFromClass( + path: NodePath, + classId: string, + typeId: string, + className: string, + export_?: Export | undefined + ): void { + const target: ClassTarget = { + id: classId, + typeId: typeId, + name: className, + type: TargetType.CLASS, + exported: !!export_, + default: export_ ? export_.default : false, + module: export_ ? export_.module : false, + }; - this.subTargets.push(target); + this._subTargets.push(target); - break; + const body = >path.get("body"); + for (const classBodyAttribute of body.get("body")) { + if (classBodyAttribute.isClassMethod()) { + if (classBodyAttribute.node.key.type !== "Identifier") { + // e.g. class A { ?() {} } + // unsupported + // not possible i think + throw new Error("unknown class method key"); } - case "LogicalExpression": - case "ConditionalExpression": { - let parent = path.parentPath; - const export_ = this._getExport(this._getNodeId(parent)); - while (parent.isLogicalExpression() || parent.isConditional()) { - parent = parent.parentPath; - } + const targetName = classBodyAttribute.node.key.name; - const target: FunctionTarget = { - id: `${this._getNodeId(parent)}`, - name: targetName, - type: TargetType.FUNCTION, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - isAsync: path.node.async, - }; + const id = this._getNodeId(classBodyAttribute); - this.subTargets.push(target); - } - default: { - const export_ = this._getExport(this._getNodeId(path)); - - const target: FunctionTarget = { - id: `${this._getNodeId(path)}`, - name: targetName, - type: TargetType.FUNCTION, - exported: !!export_, - default: export_ ? export_.default : false, - module: export_ ? export_.module : false, - isAsync: path.node.async, - }; + this._extractFromFunction( + classBodyAttribute, + id, + id, + targetName, + undefined, + false, + true, + classId + ); + } else if (classBodyAttribute.isClassProperty()) { + const key = classBodyAttribute.get("key"); + const value = classBodyAttribute.get("value"); + + if (value) { + const id = this._getNodeId(classBodyAttribute); + let targetName: string; + if (key.isIdentifier()) { + targetName = key.node.name; + } else if ( + key.isStringLiteral() || + key.isBooleanLiteral() || + key.isNumericLiteral() || + key.isBigIntLiteral() + ) { + targetName = `${key.node.value}`; + } - this.subTargets.push(target); + if (value.isFunction()) { + this._extractFromFunction( + value, + id, + id, + targetName, + undefined, + false, + true, + classId + ); + } else if (value.isClass()) { + this._extractFromClass(value, id, id, targetName); + } else if (value.isObjectExpression()) { + this._extractFromObjectExpression(value, id, id, targetName); + } else { + // TODO + } } + } else { + TargetVisitor.LOGGER.warn( + `Unsupported class body attribute: ${classBodyAttribute.node.type}` + ); } + } } - public ArrowFunctionExpression: ( - path: NodePath - ) => void = (path) => { - this._functionExpression(path); - }; - get subTargets(): SubTarget[] { - // filter duplicates because of redefinitions - // e.g. let a = 1; a = 2; - // this would result in two subtargets with the same name "a" - // but we only want the last one - this._subTargets = this._subTargets + return this._subTargets .reverse() .filter((subTarget, index, self) => { - if ("name" in subTarget) { - return ( - index === - self.findIndex((t) => { - return ( - "name" in t && - t.id === subTarget.id && - t.type === subTarget.type && - t.name === subTarget.name && - (t.type === TargetType.METHOD - ? (t).methodType === - (subTarget).methodType && - (t).isStatic === - (subTarget).isStatic && - (t).classId === - (subTarget).classId - : true) - ); - }) - ); + if (!("name" in subTarget)) { + // paths/branches/lines are always unique + return true; } - // paths/branches/lines are always unique - return true; + // filter duplicates because of redefinitions + // e.g. let a = 1; a = 2; + // this would result in two subtargets with the same name "a" + // but we only want the last one + return ( + index === + self.findIndex((t) => { + return ( + "name" in t && + t.id === subTarget.id && + t.type === subTarget.type && + t.name === subTarget.name && + (t.type === TargetType.METHOD + ? (t).methodType === + (subTarget).methodType && + (t).isStatic === + (subTarget).isStatic && + (t).classId === + (subTarget).classId + : true) + ); + }) + ); }) .reverse(); - - return this._subTargets; } } diff --git a/libraries/analysis-javascript/lib/target/VisibilityType.ts b/libraries/analysis-javascript/lib/target/VisibilityType.ts index fc425da5f..6090501c0 100644 --- a/libraries/analysis-javascript/lib/target/VisibilityType.ts +++ b/libraries/analysis-javascript/lib/target/VisibilityType.ts @@ -17,12 +17,8 @@ */ /** - * Enum for a Visibility Types. + * Visibility Types. * * @author Dimitri Stallenberg */ -export enum VisibilityType { - PUBLIC = "public", - PRIVATE = "private", - PROTECTED = "protected", -} +export type VisibilityType = "public" | "private" | "protected"; diff --git a/libraries/analysis-javascript/lib/target/export/ExportDefaultDeclaration.ts b/libraries/analysis-javascript/lib/target/export/ExportDefaultDeclaration.ts index 2c4d58a14..b8473a17e 100644 --- a/libraries/analysis-javascript/lib/target/export/ExportDefaultDeclaration.ts +++ b/libraries/analysis-javascript/lib/target/export/ExportDefaultDeclaration.ts @@ -55,6 +55,11 @@ export function extractExportsFromExportDefaultDeclaration( id = visitor._getNodeId(path.get("declaration")); break; } + case "CallExpression": { + name = "default"; + id = visitor._getNodeId(path.get("declaration")); + break; + } default: { // we could also put anon here, but that would be a bit weird // name = "anonymous" @@ -66,7 +71,9 @@ export function extractExportsFromExportDefaultDeclaration( // export default {} // export default [] // etc. - throw new Error("Unsupported export default declaration"); + throw new Error( + `Unsupported export default declaration at ${visitor._getNodeId(path)}` + ); } } diff --git a/libraries/analysis-javascript/lib/target/export/ExportNamedDeclaration.ts b/libraries/analysis-javascript/lib/target/export/ExportNamedDeclaration.ts index 3da63a787..0373e17de 100644 --- a/libraries/analysis-javascript/lib/target/export/ExportNamedDeclaration.ts +++ b/libraries/analysis-javascript/lib/target/export/ExportNamedDeclaration.ts @@ -21,36 +21,6 @@ import * as t from "@babel/types"; import { Export } from "./Export"; import { ExportVisitor } from "./ExportVisitor"; -function extractFromIdentifier( - visitor: ExportVisitor, - filePath: string, - path: NodePath, - initPath?: NodePath -): Export { - if (initPath && initPath.isIdentifier()) { - const binding = visitor._getBindingId(initPath); - - return { - id: binding, - filePath, - name: initPath.node.name, - renamedTo: path.node.name, - default: false, - module: false, - }; - } else { - // not sure about this id - return { - id: visitor._getNodeId(path), - filePath, - name: path.node.name, - renamedTo: path.node.name, - default: false, - module: false, - }; - } -} - function extractFromObjectPattern( visitor: ExportVisitor, filePath: string, @@ -246,6 +216,8 @@ export function extractExportsFromExportNamedDeclaration( declaration.isFunctionDeclaration() || declaration.isClassDeclaration() ) { + // export function x () => + // export class x () => exports.push({ id: visitor._getNodeId(declaration), filePath, @@ -255,17 +227,70 @@ export function extractExportsFromExportNamedDeclaration( module: false, }); } else if (declaration.isVariableDeclaration()) { + // export const x = ? for (const declaration_ of declaration.get("declarations")) { const id = declaration_.get("id"); + const init = declaration_.get("init"); if (id.isIdentifier()) { - exports.push(extractFromIdentifier(visitor, filePath, id, init)); + // export const x = ? + + if (!declaration_.has("init")) { + // export let x + exports.push({ + id: visitor._getNodeId(declaration_), + filePath: filePath, + name: id.node.name, + renamedTo: id.node.name, + default: false, + module: false, + }); + continue; + } + + if (init.isIdentifier()) { + // export const x = a + exports.push({ + id: visitor._getBindingId(init), + filePath: filePath, + name: init.node.name, + renamedTo: id.node.name, + default: false, + module: false, + }); + } else if (init.isLiteral()) { + // export const x = 1 + exports.push({ + id: visitor._getNodeId(declaration_), + filePath: filePath, + name: id.node.name, + renamedTo: id.node.name, + default: false, + module: false, + }); + } else if (init.isFunction() || init.isClass()) { + // export const x = () => {} + // export const y = function () => {} + // export const z = class {} + exports.push({ + id: visitor._getNodeId(declaration_), + filePath, + name: init.has("id") + ? (>init.get("id")).node.name + : id.node.name, + renamedTo: id.node.name, + default: false, + module: false, + }); + } } else if (id.isObjectPattern()) { + // TODO verify that these work exports.push( ...extractFromObjectPattern(visitor, filePath, id, init) ); } else if (id.isArrayPattern()) { + // TODO verify that these work exports.push(...extractFromArrayPattern(visitor, filePath, id, init)); } else { throw new Error("Unsupported declaration type"); diff --git a/libraries/analysis-javascript/lib/target/export/ExportVisitor.ts b/libraries/analysis-javascript/lib/target/export/ExportVisitor.ts index 76fde50f7..2b387ef43 100644 --- a/libraries/analysis-javascript/lib/target/export/ExportVisitor.ts +++ b/libraries/analysis-javascript/lib/target/export/ExportVisitor.ts @@ -23,7 +23,7 @@ import { AbstractSyntaxTreeVisitor } from "@syntest/ast-visitor-javascript"; import { Export } from "./Export"; import { extractExportsFromExportDefaultDeclaration } from "./ExportDefaultDeclaration"; import { extractExportsFromExportNamedDeclaration } from "./ExportNamedDeclaration"; -import { extractExportsFromExpressionStatement } from "./ExpressionStatement"; +import { extractExportsFromAssignmentExpression } from "./ExpressionStatement"; export class ExportVisitor extends AbstractSyntaxTreeVisitor { private _exports: Export[]; @@ -59,21 +59,16 @@ export class ExportVisitor extends AbstractSyntaxTreeVisitor { // e.g. module.exports = ... // e.g. exports.foo = ... - public ExpressionStatement: (path: NodePath) => void = - (path) => { - if (path.node.expression.type !== "AssignmentExpression") { - return; - } - - const exports = extractExportsFromExpressionStatement( - this, - this.filePath, - path - ); - if (exports) { - this._exports.push(...exports); - } - }; + public AssignmentExpression: ( + path: NodePath + ) => void = (path) => { + const exports = extractExportsFromAssignmentExpression( + this, + this.filePath, + path + ); + this._exports.push(...exports); + }; // getters get exports(): Export[] { diff --git a/libraries/analysis-javascript/lib/target/export/ExpressionStatement.ts b/libraries/analysis-javascript/lib/target/export/ExpressionStatement.ts index f11a87a50..fe5a8ad60 100644 --- a/libraries/analysis-javascript/lib/target/export/ExpressionStatement.ts +++ b/libraries/analysis-javascript/lib/target/export/ExpressionStatement.ts @@ -18,300 +18,280 @@ import { NodePath } from "@babel/core"; import * as t from "@babel/types"; -import { shouldNeverHappen } from "@syntest/search"; import { Export } from "./Export"; import { ExportVisitor } from "./ExportVisitor"; -function getName(node: t.Node): string { - switch (node.type) { - case "Identifier": { - return node.name; - } - case "ArrayExpression": - case "ObjectExpression": { - return `${node.type}`; - } - case "FunctionExpression": { - return node.id?.name || "anonymous"; - } - case "ArrowFunctionExpression": { - return "anonymous"; - } - } +type PartialExport = PartialDefaultExport | PartialNonDefaultExport; - if (node.type.includes("Literal")) { - return `${node.type}`; - } +type PartialDefaultExport = { + default: true; +}; - // throw new Error(`Cannot get name of node of type ${node.type}`) - return "anonymous"; -} +type PartialNonDefaultExport = { + default: false; + renamedTo: string; +}; -export function extractExportsFromExpressionStatement( +export function extractExportsFromAssignmentExpression( visitor: ExportVisitor, filePath: string, - path: NodePath -): Export[] | undefined { - const expression = path.get("expression"); - if (!expression.isAssignmentExpression()) { - // cannot happen (because we check this in the visitor) - return undefined; - } + path_: NodePath +): Export[] { + const exports: Export[] = []; + const left = path_.get("left"); + const right = path_.get("right"); - const assigned = expression.node.left; - const init = expression.node.right; + const partialExport: PartialExport | false = _checkExportAndDefault(left); + if (!partialExport) { + // not an export + return []; + } - const initPath = expression.get("right"); + const module = true; - let name: string; - let default_ = false; + if (partialExport.default && right.isObjectExpression()) { + // module.exports = {...} + // exports = {...} + // so not default actually + // TODO extract the stuff from the object + const properties = right.get("properties"); + exports.push( + ..._extractObjectProperties(properties, visitor, filePath, module) + ); + } else if (partialExport.default) { + // module.exports = ? + // exports = ? + // but ? is not an object expression + const id = visitor._getBindingId(right); + const name = _getName(right); + exports.push({ + id: id, + filePath: filePath, + name: name, + renamedTo: name, // actually renamed to nothing aka default but we keep the name + default: partialExport.default, + module: module, + }); + } else { + // module.exports.? = ? + // exports.? = ? + // ? can be object but we dont care since it is not a default export + const id = visitor._getBindingId(right); + const name = _getName(right); + exports.push({ + id: id, + filePath: filePath, + name: + name === "default" + ? (partialExport).renamedTo + : name, + renamedTo: (partialExport).renamedTo, + default: partialExport.default, + module: module, + }); + } - if (assigned.type === "Identifier" && assigned.name === "exports") { - // e.g. exports = ?? - name = getName(init); - default_ = true; - } else if (assigned.type === "MemberExpression") { - const object = assigned.object; - const property = assigned.property; + return exports; +} - if (object.type === "Identifier") { - if (object.name === "exports") { - // e.g. exports.?? = ?? - if (property.type === "Identifier") { - if (assigned.computed) { - // e.g. exports[x] = ?? - // unsupported - throw new Error("Unsupported export declaration"); - } else { - // e.g. exports.x = ?? - name = property.name; - } - } else if (property.type === "StringLiteral") { - // e.g. exports["x"] = ?? - name = property.value; - } else { - // e.g. exports.x() = ?? - // unsupported - // dont think this is possible - throw new Error("Unsupported export declaration"); - } - } else if (object.name === "module") { - // e.g. module.? = ?? - if (property.type === "Identifier" && property.name === "exports") { - // e.g. module.exports = ?? - if (assigned.computed) { - // e.g. module[exports] = ?? - // unsupported - throw new Error("Unsupported export declaration"); - } - name = getName(init); - default_ = true; - } else if ( - property.type === "StringLiteral" && - property.value === "exports" - ) { - // e.g. module["exports"] = ?? - name = getName(init); - default_ = true; - } else { - // e.g. module.exports() = ?? - // e.g. module.somethingelse = ?? - // unsupported - // should this just return undefined? - // throw new Error("Unsupported export declaration"); - return undefined; - } - } else { - // e.g. somethingelse.? = ?? - // e.g. somethingelse.? = ?? - // dont care - return undefined; +function _extractObjectProperties( + properties: NodePath[], + visitor: ExportVisitor, + filePath: string, + module: boolean +): Export[] { + const default_ = false; + const exports: Export[] = []; + for (const property of properties) { + if (property.isObjectMethod()) { + // {a () {}} + const key = property.get("key"); + if (!key.isIdentifier()) { + // e.g. exports = { () {} } + // unsupported + // not possible i think + throw new Error("Unsupported export declaration"); } - } else if (object.type === "MemberExpression") { - // e.g. ??.??.?? = ?? - const higherObject = object.object; - const higherProperty = object.property; + exports.push({ + id: visitor._getNodeId(property), + filePath: filePath, + name: key.node.name, + renamedTo: key.node.name, + default: default_, + module: module, + }); + } else if (property.isObjectProperty()) { + // {a: b} + const key = property.get("key"); + const value = property.get("value"); + + let keyName: string; if ( - higherObject.type === "Identifier" && - higherObject.name === "module" && - higherProperty.type === "Identifier" && - higherProperty.name === "exports" + key.isStringLiteral() || + key.isNumericLiteral() || + key.isBooleanLiteral() || + key.isBigIntLiteral() ) { - // e.g. module.exports.?? = ?? - if (property.type === "Identifier") { - // e.g. module.exports.x = ?? - if (assigned.computed) { - // e.g. module.exports[x] = ?? - // unsupported - throw new Error("Unsupported export declaration"); - } - name = property.name; - } else if (property.type === "StringLiteral") { - // e.g. module.exports["x"] = ?? - name = property.value; - } else { - // e.g. module.exports.x() = ?? - // unsupported - // not possible i think - throw new Error("Unsupported export declaration"); - } + // e.g. exports = { "a": ? } + // e.g. exports = { 1: ? } + // e.g. exports = { true: ? } + keyName = `${key.node.value}`; + } else if (key.isIdentifier()) { + // e.g. exports = { a: ? } + keyName = key.node.name; } else { - // e.g. module.somethingelse.?? = ?? - // e.g. somethingelse.exports.?? = ?? - // e.g. somethingelse.somethingelse.?? = ?? + // e.g. exports = { ?: ? } + // unsupported + throw new Error("Unsupported export declaration"); + } - // dont care - return undefined; + if (value.isIdentifier()) { + // e.g. exports = { a: b } + exports.push({ + id: visitor._getBindingId(value), + filePath, + name: value.node.name, + renamedTo: keyName, + default: default_, + module: module, + }); + } else { + // e.g. exports = { a: 1 } + exports.push({ + id: visitor._getBindingId(value), + filePath, + name: keyName, + renamedTo: keyName, + default: default_, + module: module, + }); } } else { - return undefined; + // {...a} + // unsupport + throw new Error("Unsupported export declaration"); } - } else { - // this is probably an unrelated statement - // e.g. not an export - return undefined; } + return exports; +} - if (!name) { - // not possible - throw new Error(shouldNeverHappen("ExpressionStatement")); +function _getName(expression: NodePath): string { + if (expression.isIdentifier()) { + return expression.node.name; } - const exports: Export[] = []; + if ( + expression.isLiteral() || + expression.isArrayExpression() || + expression.isObjectExpression() + ) { + return expression.type; + } - if (default_) { - if (initPath.isObjectExpression()) { - // e.g. exports = { a: ? } - // e.g. module.exports = { a: ? } - for (const property of initPath.get("properties")) { - if (property.isObjectMethod()) { - // e.g. exports = { a() {} } + if (expression.isFunction() || expression.isClass()) { + if (!expression.has("id")) { + return expression.isFunction() ? "anonymousFunction" : "anonymousClass"; + } + const id = expression.get("id"); - const key = property.get("key"); - if (!key.isIdentifier()) { - // e.g. exports = { () {} } - // unsupported - // not possible i think - throw new Error("Unsupported export declaration"); - } + // must be identifier type if it is a nodepath + return (>id).node.name; + } - exports.push({ - id: visitor._getNodeId(property), - filePath, - name: key.node.name, - renamedTo: key.node.name, - default: false, - module: true, - }); - } else if (property.isSpreadElement()) { - // e.g. exports = { ...a } - // unsupported - throw new Error("Unsupported export declaration"); - } else if (property.isObjectProperty()) { - const keyPath = property.get("key"); - const valuePath = property.get("value"); + return "default"; +} - let key: string; - if (keyPath.node.type.includes("Literal")) { - // e.g. exports = { "a": ? } - // e.g. exports = { 1: ? } - // e.g. exports = { true: ? } - // e.g. exports = { null: ? } - // eslint-disable-next-line unicorn/no-null - key = `${"value" in keyPath.node ? keyPath.node.value : null}`; - } else if (keyPath.isIdentifier()) { - // e.g. exports = { a: ? } - key = keyPath.node.name; - } else { - // e.g. exports = { 1: ? } - // unsupported - throw new Error("Unsupported export declaration"); - } +function _checkExportAndDefault( + expression: NodePath +): false | PartialExport { + if (expression.isIdentifier() && expression.node.name === "exports") { + // exports = ? + return { default: true }; + } else if (expression.isMemberExpression()) { + // ?.? = ? + const object = expression.get("object"); + const property = expression.get("property"); - if (valuePath.isIdentifier()) { - // e.g. exports = { a: b } - // get binding of b - const bindingId = visitor._getBindingId(valuePath); - exports.push({ - id: bindingId, - filePath, - name: valuePath.node.name, - renamedTo: key, - default: false, - module: true, - }); - } else { - // e.g. exports = { a: 1 } - exports.push({ - id: visitor._getNodeId(valuePath), - filePath, - name: key, - renamedTo: key, - default: false, - module: true, - }); - } + if (object.isIdentifier()) { + // ?.? = ? + if (object.node.name === "module") { + if ( + (!expression.node.computed && + property.isIdentifier() && + property.node.name === "exports") || + (expression.node.computed && + property.isStringLiteral() && + property.node.value === "exports") + ) { + // module.exports = ? + // module['exports'] = ? + return { default: true }; + } else { + // module[exports] = ? + // TODO replace by logger + console.warn(`Unsupported syntax 'module[x] = ?'`); } + } else if (object.node.name === "exports") { + // exports.? = ? + return { + default: false, + renamedTo: _getNameOfProperty(property, expression.node.computed), + }; } - } else { - // e.g. exports = ? - - if (initPath.isIdentifier()) { - // e.g. exports = obj - // e.g. module.exports = obj - // get binding of obj - const bindingId = visitor._getBindingId(initPath); + } else if (object.isMemberExpression()) { + // ?.?.? = ? + const subObject = object.get("object"); + const subProperty = object.get("property"); - exports.push({ - id: bindingId, - filePath, - name: name, - renamedTo: name, - default: default_, - module: true, - }); - } else { - // e.g. exports = 1 - // e.g. module.exports = 1 - exports.push({ - id: visitor._getNodeId(initPath), - filePath, - name: name, - renamedTo: name, - default: default_, - module: true, - }); + if ( + subObject.isIdentifier() && + subObject.node.name === "module" && + ((!object.node.computed && + subProperty.isIdentifier() && + subProperty.node.name === "exports") || + (object.node.computed && + subProperty.isStringLiteral() && + subProperty.node.value === "exports")) + ) { + // module.exports.? = ? + // module['exports'].? = ? + // module.exports[?] = ? + return { + default: false, + renamedTo: _getNameOfProperty(property, expression.node.computed), + }; } } - } else { - // e.g. exports.a = ?? - // e.g. module.exports.a = ?? - if (init.type === "Identifier") { - // e.g. exports.a = b - // get binding of b - const bindingId = visitor._getBindingId(initPath); - exports.push({ - id: bindingId, - filePath, - name: init.name, - renamedTo: name, - default: default_, - module: true, - }); + } + + return false; +} + +function _getNameOfProperty( + property: NodePath, + computed: boolean +): string { + if (computed) { + // module.exports[?] = ? + if ( + property.isStringLiteral() || + property.isNumericLiteral() || + property.isBooleanLiteral() || + property.isBigIntLiteral() + ) { + // module.exports['x'] = ? + return `${property.node.value}`; } else { - // e.g. exports.a = 1 - exports.push({ - id: visitor._getNodeId(initPath), - filePath, - name: name, - renamedTo: name, - default: default_, - module: true, - }); + // module.exports[a] = ? + throw new Error('Unsupported syntax "module.exports[a] = ?"'); } + } else if (property.isIdentifier()) { + // module.exports.x = ? + return property.node.name; + } else { + // module.exports.? = ? + throw new Error('Unsupported syntax "module.exports.? = ?"'); } - - return exports; } diff --git a/libraries/analysis-javascript/lib/type/resolving/InferenceTypeModelFactory.ts b/libraries/analysis-javascript/lib/type/resolving/InferenceTypeModelFactory.ts index 52d480f46..3eaff0cf0 100644 --- a/libraries/analysis-javascript/lib/type/resolving/InferenceTypeModelFactory.ts +++ b/libraries/analysis-javascript/lib/type/resolving/InferenceTypeModelFactory.ts @@ -505,7 +505,8 @@ export class InferenceTypeModelFactory extends TypeModelFactory { if (involved.length === 0) { throw new Error(`Function definition has no involved elements`); } - const [functionId, ...parameters] = involved; + const functionId = relationId; + const [_identifierId, ...parameters] = involved; for (const [index, id] of parameters.entries()) { this._typeModel.addParameter(functionId, index, id); @@ -534,10 +535,16 @@ export class InferenceTypeModelFactory extends TypeModelFactory { const [objectId, propertyId] = involved; const [, originalProperty] = originalInvolved; - // TODO check if the property is array or string - const propertyElement = this.getElement(originalProperty); + // TODO + // we add these scores by default because it is likely a string/object/array + // however we should check if its one of the default properties of any of the primitives + // if that is the case we should not give it string object or array + this._typeModel.addTypeScore(objectId, TypeEnum.ARRAY); + this._typeModel.addTypeScore(objectId, TypeEnum.STRING); + this._typeModel.addTypeScore(objectId, TypeEnum.OBJECT); + if (propertyElement === undefined) { // e.g. object[b ? 1 : 0] // TODO what if the property is not an element @@ -545,6 +552,11 @@ export class InferenceTypeModelFactory extends TypeModelFactory { // e.g. object[0] // add array type to object this._typeModel.addTypeScore(objectId, TypeEnum.ARRAY); + this._typeModel.addTypeScore(objectId, TypeEnum.STRING); + } else if (propertyElement.type === ElementType.StringLiteral) { + // e.g. object["abc"] + // add array type to object + this._typeModel.addTypeScore(objectId, TypeEnum.OBJECT); } else { const propertyName = "name" in propertyElement diff --git a/libraries/analysis-javascript/lib/type/resolving/RandomTypeModelFactory.ts b/libraries/analysis-javascript/lib/type/resolving/RandomTypeModelFactory.ts deleted file mode 100644 index 0992584a3..000000000 --- a/libraries/analysis-javascript/lib/type/resolving/RandomTypeModelFactory.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020-2023 Delft University of Technology and SynTest contributors - * - * This file is part of SynTest Framework - SynTest Javascript. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Element } from "../discovery/element/Element"; -import { Relation } from "../discovery/relation/Relation"; -import { TypeModel } from "./TypeModel"; -import { TypeModelFactory } from "./TypeModelFactory"; - -export class RandomTypeModelFactory extends TypeModelFactory { - getElement(): Element { - throw new Error("Method not implemented."); - } - - getRelation(): Relation { - throw new Error("Method not implemented."); - } - - resolveTypes() { - return new TypeModel(); - } -} diff --git a/libraries/analysis-javascript/lib/type/resolving/TypeModel.ts b/libraries/analysis-javascript/lib/type/resolving/TypeModel.ts index f50409085..8a66426ff 100644 --- a/libraries/analysis-javascript/lib/type/resolving/TypeModel.ts +++ b/libraries/analysis-javascript/lib/type/resolving/TypeModel.ts @@ -85,16 +85,6 @@ export class TypeModel { parameters: new Map(), return: new Set(), }); - - // this.addTypeScore(id, TypeEnum.NUMERIC, 0.01); - // this.addTypeScore(id, TypeEnum.STRING, 0.01); - // this.addTypeScore(id, TypeEnum.BOOLEAN, 0.01); - // this.addTypeScore(id, TypeEnum.NULL, 0.01); - // this.addTypeScore(id, TypeEnum.UNDEFINED, 0.01); - // this.addTypeScore(id, TypeEnum.REGEX, 0.01); - // this.addTypeScore(id, TypeEnum.OBJECT, 0.01); - // this.addTypeScore(id, TypeEnum.ARRAY, 0.01); - // this.addTypeScore(id, TypeEnum.FUNCTION, 0.01); } private _addRelationScore(id1: string, id2: string, score: number) { @@ -220,18 +210,26 @@ export class TypeModel { id ); - if (probabilities.size === 0 || prng.nextBoolean(randomTypeProbability)) { + const genericTypes = [ + TypeEnum.ARRAY, + TypeEnum.BOOLEAN, + TypeEnum.FUNCTION, + TypeEnum.NULL, + TypeEnum.NUMERIC, + TypeEnum.INTEGER, + TypeEnum.OBJECT, + TypeEnum.REGEX, + TypeEnum.STRING, + TypeEnum.UNDEFINED, + ]; + + if (probabilities.size === 0) { + return prng.pickOne(genericTypes); + } + + if (prng.nextBoolean(randomTypeProbability)) { return prng.pickOne([ - TypeEnum.ARRAY, - TypeEnum.BOOLEAN, - TypeEnum.FUNCTION, - TypeEnum.NULL, - TypeEnum.NUMERIC, - TypeEnum.INTEGER, - TypeEnum.OBJECT, - TypeEnum.REGEX, - TypeEnum.STRING, - TypeEnum.UNDEFINED, + ...new Set([...probabilities.keys(), ...genericTypes]), ]); } @@ -274,18 +272,26 @@ export class TypeModel { id ); - if (probabilities.size === 0 || prng.nextBoolean(randomTypeProbability)) { + const genericTypes = [ + TypeEnum.ARRAY, + TypeEnum.BOOLEAN, + TypeEnum.FUNCTION, + TypeEnum.NULL, + TypeEnum.NUMERIC, + TypeEnum.INTEGER, + TypeEnum.OBJECT, + TypeEnum.REGEX, + TypeEnum.STRING, + TypeEnum.UNDEFINED, + ]; + + if (probabilities.size === 0) { + return prng.pickOne(genericTypes); + } + + if (prng.nextBoolean(randomTypeProbability)) { return prng.pickOne([ - TypeEnum.ARRAY, - TypeEnum.BOOLEAN, - TypeEnum.FUNCTION, - TypeEnum.NULL, - TypeEnum.NUMERIC, - TypeEnum.INTEGER, - TypeEnum.OBJECT, - TypeEnum.REGEX, - TypeEnum.STRING, - TypeEnum.UNDEFINED, + ...new Set([...probabilities.keys(), ...genericTypes]), ]); } diff --git a/libraries/analysis-javascript/lib/type/resolving/TypePool.ts b/libraries/analysis-javascript/lib/type/resolving/TypePool.ts index 489fb1cb1..4adef1dc8 100644 --- a/libraries/analysis-javascript/lib/type/resolving/TypePool.ts +++ b/libraries/analysis-javascript/lib/type/resolving/TypePool.ts @@ -74,10 +74,17 @@ export class TypePool { return matchingTypes; } - public getRandomMatchingType(objectType: ObjectType): DiscoveredObjectType { - const matchingTypes: DiscoveredObjectType[] = + public getRandomMatchingType( + objectType: ObjectType, + extraFilter?: (type: DiscoveredObjectType) => boolean + ): DiscoveredObjectType { + let matchingTypes: DiscoveredObjectType[] = this._getMatchingTypes(objectType); + if (extraFilter) { + matchingTypes = matchingTypes.filter((type) => extraFilter(type)); + } + if (matchingTypes.length === 0) { return undefined; } diff --git a/libraries/analysis-javascript/test/target/TargetVisitor.test.ts b/libraries/analysis-javascript/test/target/TargetVisitor.test.ts index 5542ebc80..22f0c2fd1 100644 --- a/libraries/analysis-javascript/test/target/TargetVisitor.test.ts +++ b/libraries/analysis-javascript/test/target/TargetVisitor.test.ts @@ -142,13 +142,14 @@ describe("TargetVisitor test", () => { `; const targets = targetHelper(source); - + console.log(targets); expect(targets.length).to.equal(1); checkFunction(targets[0], "name1", true, true); }); it("FunctionExpression: functions overwritten in subscope", () => { + // TODO we cannot know which one is actually exported (async or not) const source = ` let name1 = function () {} @@ -366,6 +367,7 @@ describe("TargetVisitor test", () => { expect(targets.length).to.equal(2); + console.log(targets); checkClass(targets[0], "name1", false); checkClassMethod( targets[1], @@ -389,13 +391,14 @@ describe("TargetVisitor test", () => { const targets = targetHelper(source); - expect(targets.length).to.equal(2); + expect(targets.length).to.equal(3); - checkClass(targets[0], "name1", false); + checkObject(targets[0], "obj", false); + checkClass(targets[1], "name1", false); checkClassMethod( - targets[1], + targets[2], "method1", - targets[0].id, + targets[1].id, "method", "public", false, @@ -414,13 +417,42 @@ describe("TargetVisitor test", () => { const targets = targetHelper(source); - expect(targets.length).to.equal(2); + console.log(targets); + expect(targets.length).to.equal(3); - checkClass(targets[0], "name1", false); + checkObject(targets[0], "obj", false); + checkClass(targets[1], "name1", false); checkClassMethod( - targets[1], + targets[2], "method1", - targets[0].id, + targets[1].id, + "method", + "public", + false, + false + ); + }); + + it("ArrowFunctionExpression: as class expression property where class expression is in object using literal", () => { + const source = ` + const obj = { + "name1": class name2 { + method1 = () => {} + } + } + exports = obj + `; + + const targets = targetHelper(source); + + expect(targets.length).to.equal(3); + + checkObject(targets[0], "obj", true); + checkClass(targets[1], "name1", false); + checkClassMethod( + targets[2], + "method1", + targets[1].id, "method", "public", false, @@ -433,7 +465,9 @@ describe("TargetVisitor test", () => { const x = {} x[y] = function name1() {} `; - expect(targetHelper(source)).to.deep.equal([]); + const targets = targetHelper(source); + + expect(targets.length).to.equal(1); }); it("FunctionExpression: assignment memberexpression", () => { @@ -450,7 +484,7 @@ describe("TargetVisitor test", () => { checkObjectFunction(targets[1], "y", targets[0].id, false); }); - it("ObjectFunction: assignment memberexpression using literal", () => { + it("ObjectFunction: assignment memberexpression using literal two", () => { const source = ` const x = {} x['y'] = function name1() {} @@ -461,12 +495,12 @@ describe("TargetVisitor test", () => { expect(targets.length).to.equal(3); - checkObject(targets[1], "x", false); - checkObjectFunction(targets[0], "y", targets[0].id, false); + checkObject(targets[0], "x", false); + checkObjectFunction(targets[1], "y", targets[0].id, false); checkObjectFunction(targets[2], "z", targets[0].id, true); }); - it("ObjectFunction: assignment memberexpression using literal", () => { + it("ObjectFunction: assignment memberexpression using literal with export", () => { const source = ` const x = {} x['y'] = function name1() {} @@ -481,7 +515,7 @@ describe("TargetVisitor test", () => { checkObjectFunction(targets[1], "y", targets[0].id, false); }); - it("ObjectFunction: assignment memberexpression using literal", () => { + it("ObjectFunction: assignment memberexpression using literal with module export", () => { const source = ` const x = {} x['y'] = function name1() {} diff --git a/libraries/analysis-javascript/test/target/export/ExportVisitor.test.ts b/libraries/analysis-javascript/test/target/export/ExportVisitor.test.ts index 21f095048..996de14ec 100644 --- a/libraries/analysis-javascript/test/target/export/ExportVisitor.test.ts +++ b/libraries/analysis-javascript/test/target/export/ExportVisitor.test.ts @@ -612,10 +612,10 @@ describe("ExportVisitor test", () => { expect(exports.length).to.equal(1); - expect(exports[0].name).to.equal("anonymous"); + expect(exports[0].name).to.equal("anonymousFunction"); expect(exports[0].default).to.equal(true); expect(exports[0].module).to.equal(true); - expect(exports[0].renamedTo).to.equal("anonymous"); + expect(exports[0].renamedTo).to.equal("anonymousFunction"); }); it("export module default arrow function", () => { @@ -625,10 +625,10 @@ describe("ExportVisitor test", () => { expect(exports.length).to.equal(1); - expect(exports[0].name).to.equal("anonymous"); + expect(exports[0].name).to.equal("anonymousFunction"); expect(exports[0].default).to.equal(true); expect(exports[0].module).to.equal(true); - expect(exports[0].renamedTo).to.equal("anonymous"); + expect(exports[0].renamedTo).to.equal("anonymousFunction"); }); it("export expression but not assignment", () => { @@ -659,7 +659,7 @@ describe("ExportVisitor test", () => { expect(exports.length).to.equal(1); - expect(exports[0].name).to.equal("x"); + expect(exports[0].name).to.equal("NumericLiteral"); expect(exports[0].default).to.equal(false); expect(exports[0].module).to.equal(true); expect(exports[0].renamedTo).to.equal("x"); @@ -730,13 +730,16 @@ describe("ExportVisitor test", () => { expect(exports[0].renamedTo).to.equal("a"); }); - it("export module[exports] equals a", () => { + it("export module[exports] equals c", () => { const source = ` const a = 1 module[exports] = a `; - expect(() => exportHelper(source)).throw(); + const exports = exportHelper(source); + + expect(exports.length).to.equal(0); + // but warning should be given in the logs! }); it("export module.exports.x equals a", () => { diff --git a/libraries/ast-visitor-javascript/package.json b/libraries/ast-visitor-javascript/package.json index fecc8c636..0187feb80 100644 --- a/libraries/ast-visitor-javascript/package.json +++ b/libraries/ast-visitor-javascript/package.json @@ -46,6 +46,7 @@ "test:watch": "mocha --config ../../.mocharc.json --watch" }, "dependencies": { + "@syntest/logging": "^0.1.0-beta.6", "@babel/core": "7.20.12", "@babel/traverse": "7.20.12", "@syntest/logging": "^0.1.0-beta.6", diff --git a/libraries/instrumentation-javascript/lib/instrumentation/Visitor.ts b/libraries/instrumentation-javascript/lib/instrumentation/Visitor.ts index 2e4c57771..98c36fc48 100644 --- a/libraries/instrumentation-javascript/lib/instrumentation/Visitor.ts +++ b/libraries/instrumentation-javascript/lib/instrumentation/Visitor.ts @@ -21,6 +21,7 @@ import { VisitState } from "./VisitState"; import { createHash } from "crypto"; import { NodePath, template } from "@babel/core"; import * as t from "@babel/types"; +import { Scope } from "@babel/traverse"; const name = "syntest"; @@ -244,8 +245,16 @@ function convertArrowExpression(path) { } } -function extractAndReplaceVariablesFromTest(test: NodePath) { +function extractAndReplaceVariablesFromTest( + scope: Scope, + test: NodePath +) { const variables = []; + + // the next line is a hack to ensure the test is traversed from the actual test instead of the inner stuff + // essentially the wrapper sequence expression is skipped instead of the outer test expression + test.replaceWith(t.sequenceExpression([test.node])); + test.traverse( { Identifier: { @@ -265,7 +274,7 @@ function extractAndReplaceVariablesFromTest(test: NodePath) { }, CallExpression: { enter: (p) => { - const newIdentifier = test.scope.generateUidIdentifier("meta"); + const newIdentifier = scope.generateUidIdentifier("meta"); variables.push([p.getSource(), newIdentifier.name]); p.replaceWith( @@ -280,7 +289,7 @@ function extractAndReplaceVariablesFromTest(test: NodePath) { }, MemberExpression: { enter: (p) => { - const newIdentifier = test.scope.generateUidIdentifier("meta"); + const newIdentifier = scope.generateUidIdentifier("meta"); variables.push([p.getSource(), newIdentifier.name]); p.replaceWith( @@ -348,7 +357,7 @@ function coverIfBranches(path) { const index = this.cov.newStatement(test.node.loc); const increment = this.increase("s", index, null); const testAsString = `${test.toString()}`; - const variables = extractAndReplaceVariablesFromTest(test); + const variables = extractAndReplaceVariablesFromTest(path.scope, test); const metaTracker = this.getBranchMetaTracker( branch, testAsString, @@ -424,11 +433,10 @@ function coverLoopBranch(path: NodePath) { const test = (< NodePath >path).get("test"); - + const testAsString = `${test.toString()}`; const index = this.cov.newStatement(test.node.loc); const testIncrement = this.increase("s", index, null); - const variables = extractAndReplaceVariablesFromTest(test); - const testAsString = `${test.toString()}`; + const variables = extractAndReplaceVariablesFromTest(path.scope, test); const metaTracker = this.getBranchMetaTracker( branch, testAsString, @@ -528,7 +536,7 @@ function coverTernary(path: NodePath) { const testIncrement = this.increase("s", testIndex, null); const testAsString = `${test.toString()}`; - const variables = extractAndReplaceVariablesFromTest(test); + const variables = extractAndReplaceVariablesFromTest(path.scope, test); const metaTracker = this.getBranchMetaTracker( branch, testAsString, diff --git a/libraries/search-javascript/lib/criterion/BranchDistance.ts b/libraries/search-javascript/lib/criterion/BranchDistance.ts index 31397872f..e6a2d2271 100644 --- a/libraries/search-javascript/lib/criterion/BranchDistance.ts +++ b/libraries/search-javascript/lib/criterion/BranchDistance.ts @@ -23,12 +23,15 @@ import { import { BranchDistanceVisitor } from "./BranchDistanceVisitor"; import { transformSync, traverse } from "@babel/core"; import { defaultBabelOptions } from "@syntest/analysis-javascript"; +import { Logger, getLogger } from "@syntest/logging"; export class BranchDistance extends CoreBranchDistance { + protected static LOGGER: Logger; protected stringAlphabet: string; constructor(stringAlphabet: string) { super(); + BranchDistance.LOGGER = getLogger("BranchDistance"); this.stringAlphabet = stringAlphabet; } @@ -66,6 +69,15 @@ export class BranchDistance extends CoreBranchDistance { distance = 0.999_999_999_999_999_9; } + if (distance === 0) { + // in general it should not be zero if used correctly so we give a warning + const variables_ = Object.entries(variables) + .map(([key, value]) => `${key}=${value}`) + .join(", "); + BranchDistance.LOGGER.warn( + `Calculated distance for condition '${condition}' -> ${trueOrFalse}, is zero. Variables: ${variables_}` + ); + } return distance; } } diff --git a/libraries/search-javascript/lib/criterion/BranchDistanceVisitor.ts b/libraries/search-javascript/lib/criterion/BranchDistanceVisitor.ts index 1bbac8b29..3d53f3795 100644 --- a/libraries/search-javascript/lib/criterion/BranchDistanceVisitor.ts +++ b/libraries/search-javascript/lib/criterion/BranchDistanceVisitor.ts @@ -34,7 +34,6 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor { private _valueMap: Map; private _isDistanceMap: Map; - private _distance: number; constructor( stringAlphabet: string, @@ -48,7 +47,7 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor { this._valueMap = new Map(); this._isDistanceMap = new Map(); - this._distance = -1; + BranchDistanceVisitor.LOGGER = getLogger("BranchDistanceVisitor"); for (const variable of Object.keys(this._variables)) { @@ -59,21 +58,16 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor { } _getDistance(condition: string): number { - if (!this._distance) { - if ( - !this._valueMap.has(condition) || - !this._isDistanceMap.get(condition) - ) { - // the value does not exist or is not a distance - throw new Error(shouldNeverHappen("BranchDistanceVisitor")); - } - - this._distance = this._valueMap.get(condition); + if (!this._valueMap.has(condition) || !this._isDistanceMap.get(condition)) { + // the value does not exist or is not a distance + throw new Error(shouldNeverHappen("BranchDistanceVisitor")); } - return this._distance; + + return this._valueMap.get(condition); } public Statement: (path: NodePath) => void = (path) => { + let id: string; if ( path.isConditionalExpression() || path.isIfStatement() || @@ -83,47 +77,39 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor { const test = >path.get("test"); test.visit(); - - const testId = test.toString(); - - if (this._isDistanceMap.get(testId)) { - this._distance = this._valueMap.get(path.toString()); - } else { - this._distance = this._valueMap.get(path.toString()) ? 0 : 1; - } - } - - if (path.isSwitchCase() || path.isForStatement()) { + id = test.toString(); + } else if (path.isSwitchCase() || path.isForStatement()) { const test = >path.get("test"); if (test) { test.visit(); - const testId = test.toString(); - - if (this._isDistanceMap.get(testId)) { - this._distance = this._valueMap.get(path.toString()); - } else { - this._distance = this._valueMap.get(path.toString()) ? 0 : 1; - } + id = test.toString(); } else { - this._distance = 0; + path.skip(); + return; } + } else if (path.isExpressionStatement()) { + const expression = >path.get("expression"); + expression.visit(); + id = expression.toString(); } - if (path.isExpressionStatement()) { - path.get("expression").visit(); - - const expression = >path.get("expression"); - const expressionId = expression.toString(); + // TODO for in and for of - if (this._isDistanceMap.get(expressionId)) { - this._distance = this._valueMap.get(path.toString()); + let _distance: number; + if (this._isDistanceMap.get(id)) { + _distance = this._valueMap.get(id); + } else { + if (this._inverted) { + _distance = this._valueMap.get(id) ? 1 : 0; } else { - this._distance = this._valueMap.get(path.toString()) ? 0 : 1; + _distance = this._valueMap.get(id) ? 0 : 1; } + _distance = this._normalize(_distance); } - // TODO for in and for of + this._isDistanceMap.set(id, true); + this._valueMap.set(id, _distance); path.skip(); }; @@ -470,7 +456,9 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor { value = Math.abs(leftValue - rightValue); } else if ( typeof leftValue === "string" && - typeof rightValue === "string" + typeof rightValue === "string" && + !left.toString().startsWith("typeof ") && // typeof x === 'string' (should not be compared as strings (but as enums)) + !right.toString().startsWith("typeof ") // 'string' === typeof x (should not be compared as strings (but as enums)) ) { value = this._realCodedEditDistance(leftValue, rightValue); } else if ( @@ -777,7 +765,7 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor { this._stringAlphabet.indexOf(t_index) ); } - cost = this._normalize(cost); + // cost = this._normalize(cost); } // Step 6 diff --git a/libraries/search-javascript/lib/search/JavaScriptExecutionResult.ts b/libraries/search-javascript/lib/search/JavaScriptExecutionResult.ts index f31cdf13e..48bf1cac5 100644 --- a/libraries/search-javascript/lib/search/JavaScriptExecutionResult.ts +++ b/libraries/search-javascript/lib/search/JavaScriptExecutionResult.ts @@ -22,6 +22,8 @@ export enum JavaScriptExecutionStatus { PASSED, FAILED, TIMED_OUT, + MEMORY_OVERFLOW, + INFINITE_LOOP, } /** @@ -78,6 +80,13 @@ export class JavaScriptExecutionResult implements ExecutionResult { * @inheritDoc */ public coversId(id: string): boolean { + if ( + this._status === JavaScriptExecutionStatus.INFINITE_LOOP || + this._status === JavaScriptExecutionStatus.MEMORY_OVERFLOW + ) { + return false; + } + const trace = this._traces.find((trace) => trace.id === id); if (!trace) { diff --git a/libraries/search-javascript/lib/search/crossover/TreeCrossover.ts b/libraries/search-javascript/lib/search/crossover/TreeCrossover.ts index 208080927..235144fc2 100644 --- a/libraries/search-javascript/lib/search/crossover/TreeCrossover.ts +++ b/libraries/search-javascript/lib/search/crossover/TreeCrossover.ts @@ -22,12 +22,19 @@ import { prng } from "@syntest/prng"; import { JavaScriptTestCase } from "../../testcase/JavaScriptTestCase"; import { Statement } from "../../testcase/statements/Statement"; import { ActionStatement } from "../../testcase/statements/action/ActionStatement"; +import { ConstructorCall } from "../../testcase/statements/action/ConstructorCall"; +import { ConstantObject } from "../../testcase/statements/action/ConstantObject"; -interface QueueEntry { +type SwapStatement = { parent: Statement; childIndex: number; child: Statement; -} +}; + +type MatchingPair = { + parentA: SwapStatement; + parentB: SwapStatement; +}; /** * Creates 2 children which are each other's complement with respect to their parents. @@ -39,10 +46,7 @@ interface QueueEntry { * * @return a tuple of 2 children * - * @author Annibale Panichella - * @author Dimitri Stallenberg */ -// TODO check if this still works export class TreeCrossover extends Crossover { public crossOver(parents: JavaScriptTestCase[]): JavaScriptTestCase[] { if (parents.length !== 2) { @@ -54,110 +58,109 @@ export class TreeCrossover extends Crossover { const rootB: ActionStatement[] = (parents[1].copy()) .roots; - const queueA: QueueEntry[] = []; - - for (const root of rootA) { - for (let index = 0; index < root.getChildren().length; index++) { - queueA.push({ - parent: root, - childIndex: index, - child: root.getChildren()[index], - }); - } - } - - const crossoverOptions = []; + const swapStatementsA = this.convertToSwapStatements(rootA); + const swapStatementsB = this.convertToSwapStatements(rootB); - while (queueA.length > 0) { - const pair = queueA.shift(); + const crossoverOptions: MatchingPair[] = []; - if (pair.child.hasChildren()) { - for (let index = 0; index < pair.child.getChildren().length; index++) { - queueA.push({ - parent: pair.child, - childIndex: index, - child: pair.child.getChildren()[index], - }); + for (const swapA of swapStatementsA) { + for (const swapB of swapStatementsB) { + if (!swapA.child.classType || !swapB.child.classType) { + throw new Error("All statements require a classType!"); } - } - for (const root of rootB) { - if (prng.nextBoolean(this.crossoverStatementProbability)) { - // crossover - const donorSubtrees = this.findSimilarSubtree(pair.child, root); - - for (const donorTree of donorSubtrees) { - crossoverOptions.push({ - p1: pair, - p2: donorTree, - }); + if (swapA.child.variableIdentifier === swapB.child.variableIdentifier) { + if ( + (swapA.child instanceof ConstructorCall || + swapB.child instanceof ConstructorCall || + swapA.child instanceof ConstantObject || + swapB.child instanceof ConstantObject) && // if one of the two is a constructorcall or constant object both need to be + swapA.child.classType !== swapB.child.classType + ) { + continue; } + crossoverOptions.push({ + parentA: swapA, + parentB: swapB, + }); } } } if (crossoverOptions.length > 0) { - const crossoverChoice = prng.pickOne(crossoverOptions); - const pair = crossoverChoice.p1; - const donorTree = crossoverChoice.p2; - - (pair.parent).setChild( - pair.childIndex, - donorTree.child.copy() - ); - (donorTree.parent).setChild( - donorTree.childIndex, - pair.child.copy() - ); + // TODO this ignores _crossoverStatementProbability and always picks one + + const matchingPair = prng.pickOne(crossoverOptions); + const parentA = matchingPair.parentA; + const parentB = matchingPair.parentB; + + if (parentA.parent !== undefined && parentB.parent !== undefined) { + parentA.parent.setChild(parentA.childIndex, parentB.child.copy()); + parentB.parent.setChild(parentB.childIndex, parentA.child.copy()); + } else if (parentB.parent !== undefined) { + if (!(parentB.child instanceof ActionStatement)) { + throw new TypeError( + "expected parentB child to be an actionstatement" + ); + } + rootA[parentA.childIndex] = parentB.child.copy(); + parentB.parent.setChild(parentB.childIndex, parentA.child.copy()); + } else if (parentA.parent === undefined) { + if (!(parentA.child instanceof ActionStatement)) { + throw new TypeError( + "expected parentA child to be an actionstatement" + ); + } + if (!(parentB.child instanceof ActionStatement)) { + throw new TypeError( + "expected parentB child to be an actionstatement" + ); + } + rootA[parentA.childIndex] = parentB.child.copy(); + rootB[parentB.childIndex] = parentA.child.copy(); + } else { + if (!(parentA.child instanceof ActionStatement)) { + throw new TypeError( + "expected parentA child to be an actionstatement" + ); + } + parentA.parent.setChild(parentA.childIndex, parentB.child.copy()); + rootB[parentB.childIndex] = parentA.child.copy(); + } } return [new JavaScriptTestCase(rootA), new JavaScriptTestCase(rootB)]; } - /** - * Finds a subtree in the given tree which matches the wanted gene. - * - * @param wanted the gene to match the subtree with - * @param tree the tree to search in - * - * @author Dimitri Stallenberg - */ - protected findSimilarSubtree(wanted: Statement, tree: Statement) { - const queue: QueueEntry[] = []; - const similar = []; - - for (let index = 0; index < tree.getChildren().length; index++) { - queue.push({ - parent: tree, + protected convertToSwapStatements(roots: ActionStatement[]): SwapStatement[] { + const swapStatements: SwapStatement[] = []; + + for (const [index, root] of roots.entries()) { + swapStatements.push({ + parent: undefined, childIndex: index, - child: tree.getChildren()[index], + child: root, }); } + const queue: Statement[] = [...roots]; + while (queue.length > 0) { - const pair = queue.shift(); + const statement = queue.shift(); - if (pair.child.hasChildren()) { - for (let index = 0; index < pair.child.getChildren().length; index++) { - queue.push({ - parent: pair.child, + if (statement.hasChildren()) { + for (let index = 0; index < statement.getChildren().length; index++) { + const child = statement.getChildren()[index]; + swapStatements.push({ + parent: statement, childIndex: index, - child: pair.child.getChildren()[index], + child: child, }); + queue.push(child); } } - - if (!wanted.classType || !pair.child.classType) { - throw new Error("All statements require a classType!"); - } - - // TODO not sure about the ids - if (wanted.variableIdentifier === pair.child.variableIdentifier) { - // && wanted.classType === pair.child.classType) { TODO this might be necessary - similar.push(pair); - } } - return similar; + return swapStatements; } } diff --git a/libraries/search-javascript/lib/testbuilding/ContextBuilder.ts b/libraries/search-javascript/lib/testbuilding/ContextBuilder.ts new file mode 100644 index 000000000..a1653e160 --- /dev/null +++ b/libraries/search-javascript/lib/testbuilding/ContextBuilder.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2020-2023 Delft University of Technology and SynTest contributors + * + * This file is part of SynTest Framework - SynTest Javascript. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ActionStatement } from "../testcase/statements/action/ActionStatement"; +import { Decoding } from "../testcase/statements/Statement"; +import * as path from "node:path"; +import { Export } from "@syntest/analysis-javascript"; + +type Import = RegularImport | RenamedImport; + +type RegularImport = { + name: string; + renamed: false; + module: boolean; + default: boolean; +}; + +type RenamedImport = { + name: string; + renamed: true; + renamedTo: string; + module: boolean; + default: boolean; +}; + +// TODO we can also use this to generate unique identifier for the statements itself +// TODO gather assertions here too per test case +export class ContextBuilder { + private targetRootDirectory: string; + private sourceDirectory: string; + + // name -> count + private importedNames: Map; + // // name -> import string + // private imports: Map + + // path -> [name] + private imports: Map; + + private logsPresent: boolean; + private assertionsPresent: boolean; + + // old var -> new var + private variableMap: Map; + // var -> count + private variableCount: Map; + + constructor(targetRootDirectory: string, sourceDirectory: string) { + this.targetRootDirectory = targetRootDirectory; + this.sourceDirectory = sourceDirectory; + + this.importedNames = new Map(); + this.imports = new Map(); + + this.logsPresent = false; + this.assertionsPresent = false; + + this.variableMap = new Map(); + this.variableCount = new Map(); + } + + addDecoding(decoding: Decoding) { + // This function assumes the decodings to come in order + + if (decoding.reference instanceof ActionStatement) { + const export_ = decoding.reference.export; + if (export_) { + const import_ = this._addImport(export_); + const newName = import_.renamed ? import_.renamedTo : import_.name; + decoding.decoded = decoding.decoded.replaceAll(import_.name, newName); + } + } + + const variableName = decoding.reference.varName; + if (this.variableMap.has(variableName)) { + this.variableCount.set( + variableName, + this.variableCount.get(variableName) + 1 + ); + } else { + this.variableCount.set(variableName, 0); + } + + this.variableMap.set( + variableName, + variableName + this.variableCount.get(variableName) + ); + + for (const [oldVariable, newVariable] of this.variableMap.entries()) { + decoding.decoded = decoding.decoded.replaceAll(oldVariable, newVariable); + } + } + + addLogs() { + this.logsPresent = true; + } + + addAssertions() { + this.assertionsPresent = true; + } + + private _addImport(export_: Export): Import { + const path_ = export_.filePath.replace( + path.resolve(this.targetRootDirectory), + path.join(this.sourceDirectory, path.basename(this.targetRootDirectory)) + ); + + const exportedName = export_.renamedTo; + let import_: Import = { + name: exportedName === "default" ? "defaultExport" : exportedName, + renamed: false, + default: export_.default, + module: export_.module, + }; + let newName: string = exportedName; + + if (this.imports.has(path_)) { + const foundImport = this.imports.get(path_).find((value) => { + return ( + value.name === import_.name && + value.default === import_.default && + value.module === import_.module + ); + }); + if (foundImport !== undefined) { + // already in there so we return the already found on + return foundImport; + } + } + + if (this.importedNames.has(exportedName)) { + // same name new import + const count = this.importedNames.get(exportedName); + this.importedNames.set(exportedName, count + 1); + newName = exportedName + count.toString(); + + import_ = { + name: exportedName, + renamed: true, + renamedTo: newName, + default: export_.default, + module: export_.module, + }; + } else { + this.importedNames.set(exportedName, 1); + } + + if (!this.imports.has(path_)) { + this.imports.set(path_, []); + } + + this.imports.get(path_).push(import_); + return import_; + } + + // TODO we could gather all the imports of a certain path together into one import + private _getImportString(_path: string, import_: Import): string { + if (import_.renamed) { + if (import_.module) { + return import_.default + ? `const ${import_.renamedTo} = require("${_path}");` + : `const {${import_.name}: ${import_.renamedTo}} = require("${_path}");`; + } + return import_.default + ? `import ${import_.renamedTo} from "${_path}";` + : `import {${import_.name} as ${import_.renamedTo}} from "${_path}";`; + } else { + if (import_.module) { + return import_.default + ? `const ${import_.name} = require("${_path}");` + : `const {${import_.name}} = require("${_path}");`; + } + return import_.default + ? `import ${import_.name} from "${_path}";` + : `import {${import_.name}} from "${_path}";`; + } + } + + getImports(): string[] { + const imports: string[] = []; + + for (const [path_, imports_] of this.imports.entries()) { + // TODO remove unused imports + for (const import_ of imports_) { + imports.push(this._getImportString(path_, import_)); + } + } + + if (this.assertionsPresent) { + imports.push( + `import chai from 'chai'`, + `import chaiAsPromised from 'chai-as-promised'`, + `const expect = chai.expect;`, + `chai.use(chaiAsPromised);` + ); + } + + if (this.logsPresent) { + imports.push(`import * as fs from 'fs'`); + } + // TODO other post processing? + return ( + imports + // remove duplicates + // there should not be any in theory but lets do it anyway + .filter((value, index, self) => self.indexOf(value) === index) + // sort + .sort() + ); + } +} diff --git a/libraries/search-javascript/lib/testbuilding/JavaScriptDecoder.ts b/libraries/search-javascript/lib/testbuilding/JavaScriptDecoder.ts index 44ef406a3..2fbcbf4cf 100644 --- a/libraries/search-javascript/lib/testbuilding/JavaScriptDecoder.ts +++ b/libraries/search-javascript/lib/testbuilding/JavaScriptDecoder.ts @@ -18,12 +18,12 @@ import * as path from "node:path"; -import { Export } from "@syntest/analysis-javascript"; import { Decoder } from "@syntest/search"; import { JavaScriptTestCase } from "../testcase/JavaScriptTestCase"; import { Decoding } from "../testcase/statements/Statement"; import { ActionStatement } from "../testcase/statements/action/ActionStatement"; +import { ContextBuilder } from "./ContextBuilder"; export class JavaScriptDecoder implements Decoder { private targetRootDirectory: string; @@ -44,21 +44,25 @@ export class JavaScriptDecoder implements Decoder { testCases = [testCases]; } + const context = new ContextBuilder( + this.targetRootDirectory, + sourceDirectory + ); + const tests: string[] = []; - const imports: string[] = []; for (const testCase of testCases) { const roots: ActionStatement[] = testCase.roots; const importableGenes: ActionStatement[] = []; - let statements: Decoding[] = roots.flatMap((root) => + let decodings: Decoding[] = roots.flatMap((root) => root.decode(this, testCase.id, { addLogs, exception: false, }) ); - if (statements.length === 0) { + if (decodings.length === 0) { throw new Error("No statements in test case"); } @@ -85,14 +89,15 @@ export class JavaScriptDecoder implements Decoder { // statements[index] = decoded.find((x) => x.reference === statements[index].reference) // delete statements after - statements = statements.slice(0, index + 1); + decodings = decodings.slice(0, index + 1); } - if (statements.length === 0) { - throw new Error("No statements in test case"); + if (decodings.length === 0) { + throw new Error("No statements in test case after error reduction"); } - for (const [index, value] of statements.entries()) { + for (const [index, value] of decodings.entries()) { + context.addDecoding(value); const asString = "\t\t" + value.decoded.replace("\n", "\n\t\t"); if (testString.includes(asString)) { // skip repeated statements @@ -126,28 +131,34 @@ export class JavaScriptDecoder implements Decoder { ); } - const importsOfTest = this.gatherImports( - sourceDirectory, - testString, - importableGenes - ); - imports.push(...importsOfTest); + // const importsOfTest = this.gatherImports( + // context, + // sourceDirectory, + // testString, + // importableGenes + // ); + + // for (const import_ of importsOfTest) { + // if (!imports.includes(import_)) { + // // filter duplicates + // imports.push(import_); + // } + // } if (addLogs) { - imports.push(`import * as fs from 'fs'`); + context.addLogs(); } if (testCase.assertions.size > 0) { - imports.push( - `import chai from 'chai'`, - `import chaiAsPromised from 'chai-as-promised'`, - `const expect = chai.expect;`, - `chai.use(chaiAsPromised);` - ); + context.addAssertions(); } const assertions: string[] = this.generateAssertions(testCase); + if (assertions.length > 0) { + assertions.splice(0, 0, "\n\t\t// Assertions"); + } + const body = []; if (testString.length > 0) { @@ -171,134 +182,57 @@ export class JavaScriptDecoder implements Decoder { metaCommentBlock.push(`\t\t// ${metaComment}`); } + if (metaCommentBlock.length > 0) { + metaCommentBlock.splice(0, 0, "\n\t\t// Meta information"); + } + // TODO instead of using the targetName use the function call or a better description of the test tests.push( - `\tit('test for ${targetName}', async () => {\n` + - `${metaCommentBlock.join("\n")}\n` + - `${body.join("\n\n")}` + - `\n\t});` + `${metaCommentBlock.join("\n")}\n` + + `\n\t\t// Test\n` + + `${body.join("\n\n")}` ); } - if (imports.some((x) => x.includes("import") && !x.includes("require"))) { - const importsString = - imports + const imports = context.getImports(); - // remove duplicates - .filter((value, index, self) => self.indexOf(value) === index) - .join("\n") + `\n\n`; + if (imports.some((x) => x.includes("import") && !x.includes("require"))) { + const importsString = imports.join("\n") + `\n\n`; return ( + `// Imports\n` + importsString + `describe('${targetName}', function() {\n\t` + - tests.join("\n\n") + + tests + .map( + (test) => + `\tit('test for ${targetName}', async () => {\n` + + test + + `\n\t});` + ) + .join("\n\n") + `\n})` ); } else { - const importsString = - imports - - // remove duplicates - .filter((value, index, self) => self.indexOf(value) === index) - .join("\n\t") + `\n\n`; + const importsString = `\t\t` + imports.join("\n\t\t") + `\n`; return ( `describe('${targetName}', function() {\n\t` + - importsString + - tests.join("\n\n") + + tests + .map( + (test) => + `\tit('test for ${targetName}', async () => {\n` + + `\t\t// Imports\n` + + importsString + + test + + `\n\t});` + ) + .join("\n\n") + `\n})` ); } } - gatherImports( - sourceDirectory: string, - testStrings: string[], - importableGenes: ActionStatement[] - ): string[] { - const imports: string[] = []; - const importedDependencies: Set = new Set(); - - for (const gene of importableGenes) { - // TODO how to get the export of a variable? - // the below does not work with duplicate exports - const export_: Export = gene.export; - - if (!export_) { - throw new Error( - "Cannot find an export corresponding to the importable gene: " + - gene.variableIdentifier - ); - } - - // no duplicates - if (importedDependencies.has(export_.name)) { - continue; - } - importedDependencies.add(export_.name); - - // skip non-used imports - if (!testStrings.some((s) => s.includes(export_.name))) { - continue; - } - - const importString: string = this.getImport(sourceDirectory, export_); - - if (imports.includes(importString) || importString.length === 0) { - continue; - } - - imports.push(importString); - - // let count = 0; - // for (const dependency of this.dependencies.get(importName)) { - // // no duplicates - // if (importedDependencies.has(dependency.name)) { - // continue - // } - // importedDependencies.add(dependency.name) - // - // // skip non-used imports - // if (!testStrings.find((s) => s.includes(dependency.name))) { - // continue - // } - // - // const importString: string = this.getImport(dependency); - // - // if (imports.includes(importString) || importString.length === 0) { - // continue; - // } - // - // imports.push(importString); - // - // count += 1; - // } - } - - return imports; - } - - getImport(sourceDirectory: string, dependency: Export): string { - const _path = dependency.filePath.replace( - path.resolve(this.targetRootDirectory), - path.join(sourceDirectory, path.basename(this.targetRootDirectory)) - ); - - // if (dependency.module) { - // return dependency.default - // ? `import * as ${dependency.name} from "${_path}";` - // : `import {${dependency.name}} from "${_path}";`; - // } - if (dependency.module) { - return dependency.default - ? `const ${dependency.name} = require("${_path}");` - : `const {${dependency.name}} = require("${_path}");`; - } - return dependency.default - ? `import ${dependency.name} from "${_path}";` - : `import {${dependency.name}} from "${_path}";`; - } - generateAssertions(testCase: JavaScriptTestCase): string[] { const assertions: string[] = []; if (testCase.assertions.size > 0) { diff --git a/libraries/search-javascript/lib/testbuilding/JavaScriptSuiteBuilder.ts b/libraries/search-javascript/lib/testbuilding/JavaScriptSuiteBuilder.ts index 43e6ca4ce..e32be6cbe 100644 --- a/libraries/search-javascript/lib/testbuilding/JavaScriptSuiteBuilder.ts +++ b/libraries/search-javascript/lib/testbuilding/JavaScriptSuiteBuilder.ts @@ -19,9 +19,6 @@ import * as path from "node:path"; import { Archive } from "@syntest/search"; -import { InstrumentationData } from "@syntest/instrumentation-javascript"; -import cloneDeep = require("lodash.clonedeep"); -import { Runner } from "mocha"; import { JavaScriptRunner } from "../testcase/execution/JavaScriptRunner"; import { JavaScriptTestCase } from "../testcase/JavaScriptTestCase"; @@ -99,18 +96,9 @@ export class JavaScriptSuiteBuilder { return paths; } - async runSuite(paths: string[]) { - const runner: Runner = await this.runner.run(paths); - - const stats = runner.stats; - - const instrumentationData = ( - cloneDeep( - (<{ __coverage__: InstrumentationData }>(global)).__coverage__ - ) - ); - - this.runner.resetInstrumentationData(); + async runSuite(paths: string[], amount: number) { + const { stats, instrumentationData } = await this.runner.run(paths, amount); + // TODO use the results of the tests to show some statistics return { stats, instrumentationData }; } diff --git a/libraries/search-javascript/lib/testcase/JavaScriptTestCase.ts b/libraries/search-javascript/lib/testcase/JavaScriptTestCase.ts index 010655a67..d682e5c82 100644 --- a/libraries/search-javascript/lib/testcase/JavaScriptTestCase.ts +++ b/libraries/search-javascript/lib/testcase/JavaScriptTestCase.ts @@ -44,7 +44,7 @@ export class JavaScriptTestCase extends Encoding { constructor(roots: ActionStatement[]) { super(); JavaScriptTestCase.LOGGER = getLogger(JavaScriptTestCase.name); - this._roots = [...roots]; + this._roots = roots.map((value) => value.copy()); if (roots.length === 0) { throw new Error("Requires atleast one root action statement"); @@ -55,44 +55,40 @@ export class JavaScriptTestCase extends Encoding { mutate(sampler: JavaScriptTestCaseSampler): JavaScriptTestCase { JavaScriptTestCase.LOGGER.debug(`Mutating test case: ${this._id}`); - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sample(); - } - sampler.statementPool = this._statementPool; const roots = this._roots.map((action) => action.copy()); - const finalRoots = []; - - // go over each call - for (let index = 0; index < roots.length; index++) { - if (prng.nextBoolean(1 / roots.length)) { - // Mutate this position - const choice = prng.nextDouble(); - - if (choice < 0.1) { - // 10% chance to add a root on this position - finalRoots.push(sampler.sampleRoot(), roots[index]); - } else if ( - choice < 0.2 && - (roots.length > 1 || finalRoots.length > 0) - ) { - // 10% chance to delete the root - } else { - // 80% chance to just mutate the root - finalRoots.push(roots[index].mutate(sampler, 1)); - } + + const choice = prng.nextDouble(); + + if (roots.length > 1) { + if (choice < 0.33) { + // 33% chance to add a root on this position + const index = prng.nextInt(0, roots.length); + roots.splice(index, 0, sampler.sampleRoot()); + } else if (choice < 0.66) { + // 33% chance to delete the root + const index = prng.nextInt(0, roots.length - 1); + roots.splice(index, 1); } else { - finalRoots.push(roots[index]); + // 33% chance to just mutate the root + const index = prng.nextInt(0, roots.length - 1); + roots.splice(index, 1, roots[index].mutate(sampler, 1)); + } + } else { + if (choice < 0.5) { + // 50% chance to add a root on this position + const index = prng.nextInt(0, roots.length); + roots.splice(index, 0, sampler.sampleRoot()); + } else { + // 50% chance to just mutate the root + const index = prng.nextInt(0, roots.length - 1); + roots.splice(index, 1, roots[index].mutate(sampler, 1)); } - } - // add one at the end 10% * (1 / |roots|) - if (prng.nextBoolean(0.1) && prng.nextBoolean(1 / roots.length)) { - finalRoots.push(sampler.sampleRoot()); } sampler.statementPool = undefined; - return new JavaScriptTestCase(finalRoots); + return new JavaScriptTestCase(roots); } hashCode(decoder: Decoder): number { @@ -117,6 +113,6 @@ export class JavaScriptTestCase extends Encoding { } get roots(): ActionStatement[] { - return [...this._roots]; + return this._roots.map((value) => value.copy()); } } diff --git a/libraries/search-javascript/lib/testcase/StatementPool.ts b/libraries/search-javascript/lib/testcase/StatementPool.ts index 188bd78b6..cd6da9a00 100644 --- a/libraries/search-javascript/lib/testcase/StatementPool.ts +++ b/libraries/search-javascript/lib/testcase/StatementPool.ts @@ -15,20 +15,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { TypeEnum } from "@syntest/analysis-javascript"; import { ActionStatement } from "./statements/action/ActionStatement"; import { Statement } from "./statements/Statement"; import { prng } from "@syntest/prng"; import { ConstructorCall } from "./statements/action/ConstructorCall"; +import { ConstantObject } from "./statements/action/ConstantObject"; +import { FunctionCall } from "./statements/action/FunctionCall"; +import { ClassActionStatement } from "./statements/action/ClassActionStatement"; +import { ObjectFunctionCall } from "./statements/action/ObjectFunctionCall"; export class StatementPool { // type -> statement array private pool: Map; + // this is a bit out of scope for this class but otherwise we have to walk the tree multiple times + // we can solve this by making a singular tree walker class with visitors + private constructors: ConstructorCall[]; + private objects: ConstantObject[]; constructor(roots: ActionStatement[]) { this.pool = new Map(); - + this.constructors = []; + this.objects = []; this._fillGenePool(roots); } @@ -42,6 +49,27 @@ export class StatementPool { return prng.pickOne(statements); } + public getRandomConstructor(exportId?: string): ConstructorCall { + const options = exportId + ? this.constructors.filter((o) => exportId === o.export.id) + : this.constructors; + + if (options.length === 0) { + return undefined; + } + return prng.pickOne(options); + } + + public getRandomConstantObject(exportId: string): ConstantObject { + const options = exportId + ? this.objects.filter((o) => exportId === o.export.id) + : this.objects; + if (options.length === 0) { + return undefined; + } + return prng.pickOne(options); + } + private _fillGenePool(roots: ActionStatement[]) { for (const action of roots) { const queue: Statement[] = [action]; @@ -49,33 +77,36 @@ export class StatementPool { while (queue.length > 0) { const statement = queue.pop(); - if (statement.type === TypeEnum.OBJECT) { - // use type identifier - if (!this.pool.has(statement.typeIdentifier)) { - this.pool.set(statement.typeIdentifier, []); - } - this.pool.get(statement.typeIdentifier).push(statement); - } else if (statement.type === TypeEnum.FUNCTION) { - // use return type - if (statement instanceof ConstructorCall) { - if (!this.pool.has(statement.classIdentifier)) { - this.pool.set(statement.classIdentifier, []); - } - this.pool.get(statement.classIdentifier).push(statement); - } + if (statement.hasChildren()) { + queue.push(...statement.getChildren()); + } + + // use type enum for primitives and arrays + let type = statement.type; - // TODO other function return types - } else { - // use type enum for primitives and arrays - if (!this.pool.has(statement.type)) { - this.pool.set(statement.type, []); - } - this.pool.get(statement.type).push(statement); + if (statement instanceof ConstantObject) { + // use export identifier + type = statement.export.id; + this.objects.push(statement); + } else if (statement instanceof ConstructorCall) { + // use export identifier + type = statement.export.id; + this.constructors.push(statement); + } else if ( + statement instanceof FunctionCall || + statement instanceof ClassActionStatement || + statement instanceof ObjectFunctionCall + ) { + // TODO use return type + // type = statement. + // skip for now + continue; } - if (statement.hasChildren()) { - queue.push(...statement.getChildren()); + if (!this.pool.has(type)) { + this.pool.set(type, []); } + this.pool.get(type).push(statement); } } } diff --git a/libraries/search-javascript/lib/testcase/execution/ExecutionInformationIntegrator.ts b/libraries/search-javascript/lib/testcase/execution/ExecutionInformationIntegrator.ts index 5c7fafda0..49be993c5 100644 --- a/libraries/search-javascript/lib/testcase/execution/ExecutionInformationIntegrator.ts +++ b/libraries/search-javascript/lib/testcase/execution/ExecutionInformationIntegrator.ts @@ -21,6 +21,7 @@ import Mocha = require("mocha"); import { JavaScriptTestCase } from "../JavaScriptTestCase"; import { Statement } from "../statements/Statement"; import { TypeModel } from "@syntest/analysis-javascript"; +import { Test } from "./TestExecutor"; export class ExecutionInformationIntegrator { private _typeModel: TypeModel; @@ -29,11 +30,7 @@ export class ExecutionInformationIntegrator { this._typeModel = typeModel; } - process( - testCase: JavaScriptTestCase, - testResult: Mocha.Test, - stats: Mocha.Stats - ) { + process(testCase: JavaScriptTestCase, testResult: Test, stats: Mocha.Stats) { if (stats.failures === 0) { return; } @@ -45,7 +42,7 @@ export class ExecutionInformationIntegrator { const children = root.getChildren(); for (const child of children) { - if (testResult.err.message.includes(child.name)) { + if (testResult.exception && testResult.exception.includes(child.name)) { this._typeModel.addExecutionScore( child.variableIdentifier, child.type, diff --git a/libraries/search-javascript/lib/testcase/execution/JavaScriptRunner.ts b/libraries/search-javascript/lib/testcase/execution/JavaScriptRunner.ts index 76935f4b7..daf5a9ea3 100644 --- a/libraries/search-javascript/lib/testcase/execution/JavaScriptRunner.ts +++ b/libraries/search-javascript/lib/testcase/execution/JavaScriptRunner.ts @@ -19,14 +19,7 @@ import * as path from "node:path"; import { Datapoint, EncodingRunner, ExecutionResult } from "@syntest/search"; -import { - InstrumentationData, - MetaData, -} from "@syntest/instrumentation-javascript"; import { getLogger, Logger } from "@syntest/logging"; -import cloneDeep = require("lodash.clonedeep"); -import { Runner } from "mocha"; -import Mocha = require("mocha"); import { JavaScriptExecutionResult, @@ -37,79 +30,103 @@ import { JavaScriptDecoder } from "../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCase } from "../JavaScriptTestCase"; import { ExecutionInformationIntegrator } from "./ExecutionInformationIntegrator"; -// import { SilentMochaReporter } from "./SilentMochaReporter"; import { StorageManager } from "@syntest/storage"; +import { DoneMessage, Message } from "./TestExecutor"; +import { ChildProcess, fork } from "node:child_process"; +import { + InstrumentationData, + MetaData, +} from "@syntest/instrumentation-javascript"; export class JavaScriptRunner implements EncodingRunner { protected static LOGGER: Logger; protected storageManager: StorageManager; protected decoder: JavaScriptDecoder; - protected tempTestDirectory: string; protected executionInformationIntegrator: ExecutionInformationIntegrator; + protected tempTestDirectory: string; + + protected executionTimeout: number; + protected testTimeout: number; + + private _process: ChildProcess; + constructor( storageManager: StorageManager, decoder: JavaScriptDecoder, executionInformationIntergrator: ExecutionInformationIntegrator, - temporaryTestDirectory: string + temporaryTestDirectory: string, + executionTimeout: number, + testTimeout: number ) { JavaScriptRunner.LOGGER = getLogger(JavaScriptRunner.name); this.storageManager = storageManager; this.decoder = decoder; this.executionInformationIntegrator = executionInformationIntergrator; this.tempTestDirectory = temporaryTestDirectory; + this.executionTimeout = executionTimeout; + this.testTimeout = testTimeout; - process.on("uncaughtException", (reason) => { - throw reason; - }); - process.on("unhandledRejection", (reason) => { - throw reason; - }); + // eslint-disable-next-line unicorn/prefer-module + this._process = fork(path.join(__dirname, "TestExecutor.js")); } - async run(paths: string[]): Promise { + async run( + paths: string[], + amount = 1 + ): Promise> { + if (amount < 1) { + throw new Error(`Amount of tests cannot be smaller than 1`); + } paths = paths.map((p) => path.resolve(p)); - const argv: Mocha.MochaOptions = ({ - spec: paths, - // reporter: SilentMochaReporter, - diff: true, - checkLeaks: true, - slow: 1, - timeout: 1, - - watch: false, - parallel: false, - recursive: false, - sort: false, - }); + if (!this._process.connected || this._process.killed) { + // eslint-disable-next-line unicorn/prefer-module + this._process = fork(path.join(__dirname, "TestExecutor.js")); + } - const mocha = new Mocha(argv); // require('ts-node/register') + const childProcess = this._process; + + return await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + JavaScriptRunner.LOGGER.warn( + `Execution timeout reached killing process, timeout: ${this.executionTimeout} times ${amount}` + ); + childProcess.removeAllListeners(); + childProcess.kill(); + reject("timeout"); + }, this.executionTimeout * amount); + + childProcess.on("message", (data: Message) => { + if (typeof data !== "object") { + return reject( + new TypeError("Invalid data received from child process") + ); + } - // eslint-disable-next-line unicorn/prefer-module - require("regenerator-runtime/runtime"); - // eslint-disable-next-line unicorn/prefer-module, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires - require("@babel/register")({ - // eslint-disable-next-line unicorn/prefer-module - presets: [require.resolve("@babel/preset-env")], - }); + if (data.message === "done") { + childProcess.removeAllListeners(); + clearTimeout(timeout); - for (const _path of paths) { - // eslint-disable-next-line unicorn/prefer-module - delete require.cache[_path]; - mocha.addFile(_path); - } + if (!data.instrumentationData) { + return reject("no instrumentation data found"); + } - let runner: Runner; + return resolve(data); + } + }); - // Finally, run mocha. - await new Promise((resolve) => { - runner = mocha.run((failures) => resolve(failures)); - }); + childProcess.on("error", (error) => { + reject(error); + }); - mocha.dispose(); - return runner; + childProcess.send({ + message: "run", + paths: paths, + timeout: this.testTimeout, + }); + }); } async execute( @@ -127,23 +144,59 @@ export class JavaScriptRunner implements EncodingRunner { true ); - const runner = await this.run([testPath]); - const test = runner.suite.suites[0].tests[0]; - const stats = runner.stats; + let executionResult: JavaScriptExecutionResult; + const last = Date.now(); + try { + const { suites, stats, instrumentationData, metaData } = await this.run([ + testPath, + ]); + JavaScriptRunner.LOGGER.debug(`test run took: ${Date.now() - last} ms`); + const test = suites[0].tests[0]; // only one test in this case + + // If one of the executions failed, log it + this.executionInformationIntegrator.process(testCase, test, stats); + + const traces: Datapoint[] = this._extractTraces( + instrumentationData, + metaData + ); - // If one of the executions failed, log it - this.executionInformationIntegrator.process(testCase, test, stats); + // Retrieve execution information + executionResult = new JavaScriptExecutionResult( + test.status, + traces, + test.duration, + test.exception + ); + } catch (error) { + if (error === "timeout") { + // we put undefined as exception such that the test case doesnt end up in the final test suite + JavaScriptRunner.LOGGER.debug(`test run took: ${Date.now() - last} ms`); + executionResult = new JavaScriptExecutionResult( + JavaScriptExecutionStatus.INFINITE_LOOP, + [], + -1, + undefined + ); + } else { + JavaScriptRunner.LOGGER.error(error); + throw error; + } + } - // Retrieve execution traces - const instrumentationData = ( - cloneDeep( - (<{ __coverage__: InstrumentationData }>(global)).__coverage__ - ) - ); - const metaData = ( - cloneDeep((<{ __meta__: MetaData }>(global)).__meta__) + // Remove test file + this.storageManager.deleteTemporary( + [this.tempTestDirectory], + "tempTest.spec.js" ); + return executionResult; + } + + private _extractTraces( + instrumentationData: InstrumentationData, + metaData: MetaData + ): Datapoint[] { const traces: Datapoint[] = []; for (const key of Object.keys(instrumentationData)) { @@ -260,72 +313,10 @@ export class JavaScriptRunner implements EncodingRunner { } } - // Retrieve execution information - let executionResult: JavaScriptExecutionResult; - if ( - runner.suite.suites.length > 0 && - runner.suite.suites[0].tests.length > 0 - ) { - const test = runner.suite.suites[0].tests[0]; - - let status: JavaScriptExecutionStatus; - let exception: string; - - if (test.isPassed()) { - status = JavaScriptExecutionStatus.PASSED; - } else if (test.timedOut) { - status = JavaScriptExecutionStatus.TIMED_OUT; - } else { - status = JavaScriptExecutionStatus.FAILED; - exception = test.err.message; - } - - const duration = test.duration; - - executionResult = new JavaScriptExecutionResult( - status, - traces, - duration, - exception - ); - } else { - executionResult = new JavaScriptExecutionResult( - JavaScriptExecutionStatus.FAILED, - traces, - stats.duration - ); - } - - // Reset instrumentation data (no hits) - this.resetInstrumentationData(); - - // Remove test file - this.storageManager.deleteTemporary( - [this.tempTestDirectory], - "tempTest.spec.js" - ); - - return executionResult; + return traces; } - resetInstrumentationData() { - const coverage = (<{ __coverage__: InstrumentationData }>(global)) - .__coverage__; - - if (coverage === undefined) { - return; - } - - for (const key of Object.keys(coverage)) { - for (const statementKey of Object.keys(coverage[key].s)) { - coverage[key].s[statementKey] = 0; - } - for (const functionKey of Object.keys(coverage[key].f)) { - coverage[key].f[functionKey] = 0; - } - for (const branchKey of Object.keys(coverage[key].b)) { - coverage[key].b[branchKey] = [0, 0]; - } - } + get process() { + return this._process; } } diff --git a/libraries/search-javascript/lib/testcase/execution/TestExecutor.ts b/libraries/search-javascript/lib/testcase/execution/TestExecutor.ts new file mode 100644 index 000000000..179669744 --- /dev/null +++ b/libraries/search-javascript/lib/testcase/execution/TestExecutor.ts @@ -0,0 +1,152 @@ +/* + * Copyright 2020-2023 Delft University of Technology and SynTest contributors + * + * This file is part of SynTest Framework - SynTest JavaScript. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Runner } from "mocha"; +import Mocha = require("mocha"); +import { JavaScriptExecutionStatus } from "../../search/JavaScriptExecutionResult"; +import { + InstrumentationData, + MetaData, +} from "@syntest/instrumentation-javascript"; +import cloneDeep = require("lodash.clonedeep"); +import { SilentMochaReporter } from "./SilentMochaReporter"; + +export type Message = RunMessage | DoneMessage; + +export type RunMessage = { + message: "run"; + paths: string[]; + timeout: number; +}; + +export type DoneMessage = { + message: "done"; + suites: Suite[]; + stats: Mocha.Stats; + instrumentationData: InstrumentationData; + metaData: MetaData; + error?: string; +}; + +export type Suite = { + tests: Test[]; +}; + +export type Test = { + status: JavaScriptExecutionStatus; + exception?: string; + duration: number; +}; + +process.on("uncaughtException", (reason) => { + throw reason; +}); +process.on("unhandledRejection", (reason) => { + throw reason; +}); + +process.on("message", async (data: Message) => { + if (typeof data !== "object") { + console.log(data); + throw new TypeError("Invalid data received from child process"); + } + if (data.message === "run") { + await runMocha(data.paths, data.timeout); + } +}); + +async function runMocha(paths: string[], timeout: number) { + const argv: Mocha.MochaOptions = ({ + reporter: SilentMochaReporter, + // diff: false, + // checkLeaks: false, + // slow: 75, + timeout: timeout, + + // watch: false, + // parallel: false, + // recursive: false, + // sort: false, + }); + + const mocha = new Mocha(argv); // require('ts-node/register') + // eslint-disable-next-line unicorn/prefer-module + require("regenerator-runtime/runtime"); + // eslint-disable-next-line unicorn/prefer-module, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires + require("@babel/register")({ + // eslint-disable-next-line unicorn/prefer-module + presets: [require.resolve("@babel/preset-env")], + }); + + for (const _path of paths) { + // eslint-disable-next-line unicorn/prefer-module + delete require.cache[_path]; + mocha.addFile(_path); + } + + let runner: Runner; + + // Finally, run mocha. + await new Promise((resolve) => { + runner = mocha.run((failures) => resolve(failures)); + }); + + const suites: Suite[] = runner.suite.suites.map((suite) => { + return { + tests: suite.tests.map((test) => { + let status: JavaScriptExecutionStatus; + if (test.isPassed()) { + status = JavaScriptExecutionStatus.PASSED; + } else if (test.timedOut) { + status = JavaScriptExecutionStatus.TIMED_OUT; + } else { + status = JavaScriptExecutionStatus.FAILED; + } + return { + status: status, + exception: + status === JavaScriptExecutionStatus.FAILED + ? test.err.message + : undefined, + duration: test.duration, + }; + }), + }; + }); + + // Retrieve execution traces + const instrumentationData = ( + cloneDeep( + (<{ __coverage__: InstrumentationData }>(global)).__coverage__ + ) + ); + const metaData = ( + cloneDeep((<{ __meta__: MetaData }>(global)).__meta__) + ); + + const result: DoneMessage = { + message: "done", + suites: suites, + stats: runner.stats, + instrumentationData: instrumentationData, + metaData: metaData, + }; + process.send(result); + + mocha.dispose(); +} diff --git a/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts b/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts index b0cabb4f1..ae6e80298 100644 --- a/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts +++ b/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts @@ -61,34 +61,36 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { constantPoolManager: ConstantPoolManager, constantPoolEnabled: boolean, constantPoolProbability: number, + typePoolEnabled: boolean, + typePoolProbability: number, + statementPoolEnabled: boolean, + statementPoolProbability: number, typeInferenceMode: string, randomTypeProbability: number, incorporateExecutionInformation: boolean, maxActionStatements: number, stringAlphabet: string, stringMaxLength: number, - resampleGeneProbability: number, deltaMutationProbability: number, - exploreIllegalValues: boolean, - reuseStatementProbability: number, - useMockedObjectProbability: number + exploreIllegalValues: boolean ) { super( subject, constantPoolManager, constantPoolEnabled, constantPoolProbability, + typePoolEnabled, + typePoolProbability, + statementPoolEnabled, + statementPoolProbability, typeInferenceMode, randomTypeProbability, incorporateExecutionInformation, maxActionStatements, stringAlphabet, stringMaxLength, - resampleGeneProbability, deltaMutationProbability, - exploreIllegalValues, - reuseStatementProbability, - useMockedObjectProbability + exploreIllegalValues ); } @@ -97,7 +99,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { for ( let index = 0; - index < prng.nextInt(1, this.maxActionStatements); + index < prng.nextInt(1, this.maxActionStatements); // (i think its better to start with a single statement) index++ ) { this.statementPool = new StatementPool(roots); @@ -111,6 +113,45 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { sampleRoot(): ActionStatement { const targets = (this._subject).getActionableTargets(); + if (this.statementPoolEnabled) { + const constructor_ = this.statementPool.getRandomConstructor(); + + if (constructor_ && prng.nextBoolean(this.statementPoolProbability)) { + // TODO ignoring getters and setters for now + const targets = this.rootContext.getSubTargets( + constructor_.typeIdentifier.split(":")[0] + ); + const methods = ( + targets.filter( + (target) => + target.type === TargetType.METHOD && + (target).methodType === "method" && + (target).classId === constructor_.classIdentifier + ) + ); + if (methods.length > 0) { + const method = prng.pickOne(methods); + + const type_ = this.rootContext + .getTypeModel() + .getObjectDescription(method.typeId); + + const arguments_: Statement[] = + this.methodCallGenerator.sampleArguments(0, type_); + + return new MethodCall( + method.id, + method.typeId, + method.name, + TypeEnum.FUNCTION, + prng.uniqueId(), + arguments_, + constructor_ + ); + } + } + } + const action = prng.pickOne( targets.filter( (target) => @@ -167,7 +208,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return this.functionCallGenerator.generate( depth, function_.id, - function_.id, + function_.typeId, function_.id, function_.name, this.statementPool @@ -224,7 +265,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return new ConstructorCall( class_.id, - class_.id, + class_.typeId, class_.id, class_.name, TypeEnum.FUNCTION, @@ -237,7 +278,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return this.constructorCallGenerator.generate( depth, action.id, - action.id, + (action).typeId, class_.id, class_.name, this.statementPool @@ -289,7 +330,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return this.methodCallGenerator.generate( depth, method.id, - method.id, + method.typeId, class_.id, method.name, this.statementPool @@ -325,7 +366,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return this.setterGenerator.generate( depth, method.id, - method.id, + method.typeId, class_.id, method.name, this.statementPool @@ -364,7 +405,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return this.constantObjectGenerator.generate( depth, object_.id, - object_.id, + object_.typeId, object_.id, object_.name, this.statementPool @@ -382,7 +423,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { return this.objectFunctionCallGenerator.generate( depth, randomFunction.id, - randomFunction.id, + randomFunction.typeId, object_.id, randomFunction.name, this.statementPool @@ -410,12 +451,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { // TODO should be done in the typemodel somehow // maybe create types for the subproperties by doing /main/array/id::1::1[element-index] // maybe create types for the subproperties by doing /main/array/id::1::1.property - return this.sampleString( - "anon", - "anon", - this.stringAlphabet, - this.stringMaxLength - ); + return this.sampleArgument(depth, "anon", "anon"); } return this.sampleArgument(depth, prng.pickOne(childIds), String(index)); @@ -423,16 +459,18 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { sampleObjectArgument( depth: number, - objectId: string, + objectTypeId: string, property: string ): Statement { const objectType = ( - this.rootContext.getTypeModel().getObjectDescription(objectId) + this.rootContext.getTypeModel().getObjectDescription(objectTypeId) ); const value = objectType.properties.get(property); if (!value) { - throw new Error(`Property ${property} not found in object ${objectId}`); + throw new Error( + `Property ${property} not found in object ${objectTypeId}` + ); } return this.sampleArgument(depth, value, property); @@ -441,27 +479,41 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { sampleArgument(depth: number, id: string, name: string): Statement { let chosenType: string; - if ( - this.typeInferenceMode === "proportional" || - this.typeInferenceMode === "none" - ) { - chosenType = this.rootContext - .getTypeModel() - .getRandomType( - this.incorporateExecutionInformation, - this.randomTypeProbability, - id - ); - } else if (this.typeInferenceMode === "ranked") { - chosenType = this.rootContext - .getTypeModel() - .getHighestProbabilityType( - this.incorporateExecutionInformation, - this.randomTypeProbability, - id + switch (this.typeInferenceMode) { + case "none": { + chosenType = this.rootContext + .getTypeModel() + .getRandomType(false, 1, id); + + break; + } + case "proportional": { + chosenType = this.rootContext + .getTypeModel() + .getRandomType( + this.incorporateExecutionInformation, + this.randomTypeProbability, + id + ); + + break; + } + case "ranked": { + chosenType = this.rootContext + .getTypeModel() + .getHighestProbabilityType( + this.incorporateExecutionInformation, + this.randomTypeProbability, + id + ); + + break; + } + default: { + throw new Error( + "Invalid identifierDescription inference mode selected" ); - } else { - throw new Error("Invalid identifierDescription inference mode selected"); + } } if (chosenType.endsWith("object")) { @@ -473,10 +525,16 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { } // take from pool - const statementFromPool = this.statementPool.getRandomStatement(chosenType); - - if (statementFromPool && prng.nextBoolean(this.reuseStatementProbability)) { - return statementFromPool; + if (this.statementPoolEnabled) { + const statementFromPool = + this.statementPool.getRandomStatement(chosenType); + + if ( + statementFromPool && + prng.nextBoolean(this.statementPoolProbability) + ) { + return statementFromPool; + } } switch (chosenType) { @@ -514,87 +572,94 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { .getTypeModel() .getObjectDescription(typeId); - const typeFromTypePool = this.rootContext - .getTypePool() - .getRandomMatchingType(typeObject); + if (this.typePoolEnabled) { + // TODO maybe we should sample from the typepool for the other stuff as well (move this to sample arg for example) + const typeFromTypePool = this.rootContext + .getTypePool() + // .getRandomMatchingType(typeObject) + // TODO this prevents ONLY allows sampling of matching class constructors + .getRandomMatchingType( + typeObject, + (type_) => type_.kind === DiscoveredObjectKind.CLASS + ); - if ( - typeFromTypePool && - prng.nextBoolean(1 - this.useMockedObjectProbability) - ) { - // always prefer type from type pool - switch (typeFromTypePool.kind) { - case DiscoveredObjectKind.CLASS: { - // find constructor of class - const targets = this.rootContext.getSubTargets( - typeFromTypePool.id.split(":")[0] - ); - const constructor_ = targets.find( - (target) => - target.type === TargetType.METHOD && - (target).methodType === "constructor" && - (target).classId === typeFromTypePool.id - ); + if (typeFromTypePool && prng.nextBoolean(this.typePoolProbability)) { + // always prefer type from type pool + switch (typeFromTypePool.kind) { + case DiscoveredObjectKind.CLASS: { + // find constructor of class + const targets = this.rootContext.getSubTargets( + typeFromTypePool.id.split(":")[0] + ); + const constructor_ = ( + targets.find( + (target) => + target.type === TargetType.METHOD && + (target).methodType === "constructor" && + (target).classId === typeFromTypePool.id + ) + ); + + if (constructor_) { + return this.constructorCallGenerator.generate( + depth, + id, // variable id + constructor_.typeId, // constructor call id + typeFromTypePool.id, // class export id + name, + this.statementPool + ); + } - if (constructor_) { return this.constructorCallGenerator.generate( depth, id, // variable id - constructor_.id, // constructor call id + typeFromTypePool.id, // constructor call id typeFromTypePool.id, // class export id name, this.statementPool ); } - - return this.constructorCallGenerator.generate( - depth, - id, // variable id - typeFromTypePool.id, // constructor call id - typeFromTypePool.id, // class export id - name, - this.statementPool - ); - } - case DiscoveredObjectKind.FUNCTION: { - return this.functionCallGenerator.generate( - depth, - id, - typeFromTypePool.id, - typeFromTypePool.id, - name, - this.statementPool - ); - } - case DiscoveredObjectKind.INTERFACE: { - // TODO - return this.constructorCallGenerator.generate( - depth, - id, - typeFromTypePool.id, - typeFromTypePool.id, - name, - this.statementPool - ); - } - case DiscoveredObjectKind.OBJECT: { - return this.constantObjectGenerator.generate( - depth, - id, - typeFromTypePool.id, - typeFromTypePool.id, - name, - this.statementPool - ); + case DiscoveredObjectKind.FUNCTION: { + return this.functionCallGenerator.generate( + depth, + id, + typeFromTypePool.id, + typeFromTypePool.id, + name, + this.statementPool + ); + } + case DiscoveredObjectKind.INTERFACE: { + // TODO + return this.constructorCallGenerator.generate( + depth, + id, + typeFromTypePool.id, + typeFromTypePool.id, + name, + this.statementPool + ); + } + case DiscoveredObjectKind.OBJECT: { + return this.constantObjectGenerator.generate( + depth, + id, + typeFromTypePool.id, + typeFromTypePool.id, + name, + this.statementPool + ); + } + // No default } - // No default } } const object_: { [key: string]: Statement } = {}; - for (const [key, id] of typeObject.properties.entries()) { - object_[key] = this.sampleArgument(depth + 1, id, key); + for (const key of typeObject.properties.keys()) { + object_[key] = this.sampleObjectArgument(depth + 1, typeId, key); } return new ObjectStatement( @@ -625,14 +690,7 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { // maybe create types for the subproperties by doing /main/array/id::1::1.property if (children.length === 0) { - children.push( - this.sampleString( - "anon", - "anon", - this.stringAlphabet, - this.stringMaxLength - ) - ); + children.push(this.sampleArrayArgument(depth + 1, id, 0)); } // if some children are missing, fill them with fake params @@ -764,8 +822,8 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { sampleNumber(id: string, name: string): NumericStatement { // by default we create small numbers (do we need very large numbers?) - const max = 10; - const min = -10; + const max = 1000; + const min = -1000; const value = this.constantPoolEnabled && prng.nextBoolean(this.constantPoolProbability) @@ -788,8 +846,8 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { sampleInteger(id: string, name: string): IntegerStatement { // by default we create small numbers (do we need very large numbers?) - const max = 10; - const min = -10; + const max = 1000; + const min = -1000; const value = this.constantPoolEnabled && prng.nextBoolean(this.constantPoolProbability) diff --git a/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts b/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts index d7ec5560e..fb62b54c4 100644 --- a/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts +++ b/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts @@ -60,17 +60,25 @@ export abstract class JavaScriptTestCaseSampler extends EncodingSampler { protected _sampler: JavaScriptTestCaseSampler; protected _rootContext: RootContext; - protected _reuseStatementProbability: number; + + protected _statementPoolEnabled: boolean; + protected _statementPoolProbability: number; constructor( sampler: JavaScriptTestCaseSampler, rootContext: RootContext, - reuseStatementProbability: number + statementPoolEnabled: boolean, + statementPoolProbability: number ) { this._sampler = sampler; this._rootContext = rootContext; - this._reuseStatementProbability = reuseStatementProbability; + this._statementPoolEnabled = statementPoolEnabled; + this._statementPoolProbability = statementPoolProbability; } abstract generate( @@ -52,7 +56,11 @@ export abstract class Generator { return this._rootContext; } - get reuseStatementProbability() { - return this._reuseStatementProbability; + get statementPoolEnabled() { + return this._statementPoolEnabled; + } + + get statementPoolProbability() { + return this._statementPoolProbability; } } diff --git a/libraries/search-javascript/lib/testcase/sampling/generators/action/CallGenerator.ts b/libraries/search-javascript/lib/testcase/sampling/generators/action/CallGenerator.ts index 7a563db87..90f37ace3 100644 --- a/libraries/search-javascript/lib/testcase/sampling/generators/action/CallGenerator.ts +++ b/libraries/search-javascript/lib/testcase/sampling/generators/action/CallGenerator.ts @@ -69,6 +69,13 @@ export abstract class CallGenerator extends Generator { } } + for (let index = 0; index < 10; index++) { + if (prng.nextBoolean(0.05)) { + // TODO make this a config parameter + arguments_.push(this.sampler.sampleArgument(depth + 1, "anon", "anon")); + } + } + return arguments_; } } diff --git a/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstantObjectGenerator.ts b/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstantObjectGenerator.ts index ef42606d5..fa221748e 100644 --- a/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstantObjectGenerator.ts +++ b/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstantObjectGenerator.ts @@ -34,11 +34,23 @@ export class ConstantObjectGenerator extends CallGenerator { .flat() .find((export_) => export_.id === exportIdentifier); + if (this.statementPoolEnabled) { + const statementFromPool = + statementPool.getRandomConstantObject(exportIdentifier); + + if ( + statementFromPool && + prng.nextBoolean(this.statementPoolProbability) + ) { + return statementFromPool; + } + } + return new ConstantObject( variableIdentifier, typeIdentifier, name, - TypeEnum.FUNCTION, + TypeEnum.OBJECT, prng.uniqueId(), export_ ); diff --git a/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstructorCallGenerator.ts b/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstructorCallGenerator.ts index 17c367b93..495ad2c10 100644 --- a/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstructorCallGenerator.ts +++ b/libraries/search-javascript/lib/testcase/sampling/generators/action/ConstructorCallGenerator.ts @@ -50,11 +50,16 @@ export class ConstructorCallGenerator extends CallGenerator { // ); // } - const statementFromPool = - statementPool.getRandomStatement(exportIdentifier); + if (this.statementPoolEnabled) { + const statementFromPool = + statementPool.getRandomConstructor(exportIdentifier); - if (statementFromPool && prng.nextBoolean(this.reuseStatementProbability)) { - return statementFromPool; + if ( + statementFromPool && + prng.nextBoolean(this.statementPoolProbability) + ) { + return statementFromPool; + } } const type_ = this.rootContext diff --git a/libraries/search-javascript/lib/testcase/sampling/generators/action/FunctionCallGenerator.ts b/libraries/search-javascript/lib/testcase/sampling/generators/action/FunctionCallGenerator.ts index 21d1b0bdd..a20151b87 100644 --- a/libraries/search-javascript/lib/testcase/sampling/generators/action/FunctionCallGenerator.ts +++ b/libraries/search-javascript/lib/testcase/sampling/generators/action/FunctionCallGenerator.ts @@ -39,7 +39,7 @@ export class FunctionCallGenerator extends CallGenerator { const export_ = [...this.rootContext.getAllExports().values()] .flat() - .find((export_) => export_.id === typeIdentifier); + .find((export_) => export_.id === exportIdentifier); return new FunctionCall( variableIdentifier, diff --git a/libraries/search-javascript/lib/testcase/statements/Statement.ts b/libraries/search-javascript/lib/testcase/statements/Statement.ts index 3bc1b8dd3..17062e8b1 100644 --- a/libraries/search-javascript/lib/testcase/statements/Statement.ts +++ b/libraries/search-javascript/lib/testcase/statements/Statement.ts @@ -17,7 +17,6 @@ */ import { Encoding, EncodingSampler, shouldNeverHappen } from "@syntest/search"; -import { prng } from "@syntest/prng"; import { JavaScriptDecoder } from "../../testbuilding/JavaScriptDecoder"; @@ -29,7 +28,7 @@ export abstract class Statement { private _typeIdentifier: string; private _name: string; private _type: string; - private _uniqueId: string; + protected _uniqueId: string; protected _varName: string; protected _classType: string; @@ -85,13 +84,17 @@ export abstract class Statement { throw new Error(shouldNeverHappen("name cannot inlude <>")); } - this._varName = "_" + this.generateVarName(name, type); + this._varName = "_" + this.generateVarName(name, type, uniqueId); } - protected generateVarName(name: string, type: string): string { + protected generateVarName( + name: string, + type: string, + uniqueId: string + ): string { return type.includes("<>") - ? name + "_" + type.split("<>")[1] + "_" + prng.uniqueId(4) - : name + "_" + type + "_" + prng.uniqueId(4); + ? name + "_" + type.split("<>")[1] + "_" + uniqueId + : name + "_" + type + "_" + uniqueId; } /** @@ -120,6 +123,16 @@ export abstract class Statement { */ abstract getChildren(): Statement[]; + /** + * Set a new child at a specified position + * + * WARNING: This function has side effects + * + * @param index the index position of the new child + * @param newChild the new child + */ + abstract setChild(index: number, newChild: Statement): void; + /** * Decodes the statement */ diff --git a/libraries/search-javascript/lib/testcase/statements/action/ActionStatement.ts b/libraries/search-javascript/lib/testcase/statements/action/ActionStatement.ts index 94104f450..ee75f1eeb 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/ActionStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/ActionStatement.ts @@ -16,11 +16,10 @@ * limitations under the License. */ -import { Encoding, EncodingSampler } from "@syntest/search"; +import { Encoding, EncodingSampler, shouldNeverHappen } from "@syntest/search"; import { Statement } from "../Statement"; import { Export } from "@syntest/analysis-javascript"; -import { prng } from "@syntest/prng"; /** * @author Dimitri Stallenberg @@ -42,18 +41,22 @@ export abstract class ActionStatement extends Statement { this._args = arguments_; this._export = export_; - this._varName = "_" + this.generateVarName(name, type); + this._varName = "_" + this.generateVarName(name, type, uniqueId); } - protected override generateVarName(name: string, type: string): string { + protected override generateVarName( + name: string, + type: string, + uniqueId: string + ): string { // TODO should use return type if (this._export) { - return name + "_" + this._export.name + "_" + prng.uniqueId(4); + return name + "_" + this._export.name + "_" + uniqueId; } return type.includes("<>") - ? name + "_" + type.split("<>")[1] + "_" + prng.uniqueId(4) - : name + "_" + type + "_" + prng.uniqueId(4); + ? name + "_" + type.split("<>")[1] + "_" + uniqueId + : name + "_" + type + "_" + uniqueId; } abstract override mutate( @@ -64,6 +67,14 @@ export abstract class ActionStatement extends Statement { abstract override copy(): ActionStatement; setChild(index: number, newChild: Statement) { + if (!newChild) { + throw new Error("Invalid new child!"); + } + + if (index < 0 || index >= this.args.length) { + throw new Error(shouldNeverHappen(`Invalid index used index: ${index}`)); + } + this.args[index] = newChild; } @@ -75,7 +86,7 @@ export abstract class ActionStatement extends Statement { return [...this._args]; } - get args(): Statement[] { + protected get args(): Statement[] { return this._args; } diff --git a/libraries/search-javascript/lib/testcase/statements/action/ClassActionStatement.ts b/libraries/search-javascript/lib/testcase/statements/action/ClassActionStatement.ts index d0fa84275..2bc6896d3 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/ClassActionStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/ClassActionStatement.ts @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { shouldNeverHappen } from "@syntest/search"; import { Statement } from "../Statement"; import { ActionStatement } from "./ActionStatement"; import { ConstructorCall } from "./ConstructorCall"; @@ -43,6 +44,33 @@ export abstract class ClassActionStatement extends ActionStatement { this._constructor = constructor_; } + override setChild(index: number, newChild: Statement) { + if (!newChild) { + throw new Error("Invalid new child!"); + } + + if (index < 0 || index > this.args.length) { + throw new Error(shouldNeverHappen(`Invalid index used index: ${index}`)); + } + + if (index === this.args.length) { + if (!(newChild instanceof ConstructorCall)) { + throw new TypeError(shouldNeverHappen("should be a constructor")); + } + this._constructor = newChild; + } else { + this.args[index] = newChild; + } + } + + override hasChildren(): boolean { + return true; + } + + override getChildren(): Statement[] { + return [...this.args, this._constructor]; + } + get constructor_() { return this._constructor; } diff --git a/libraries/search-javascript/lib/testcase/statements/action/ConstantObject.ts b/libraries/search-javascript/lib/testcase/statements/action/ConstantObject.ts index a64eab3f2..f605c61a1 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/ConstantObject.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/ConstantObject.ts @@ -29,12 +29,6 @@ import { ActionStatement } from "./ActionStatement"; * @author Dimitri Stallenberg */ export class ConstantObject extends ActionStatement { - /** - * Constructor - * @param type the return identifierDescription of the constructor - * @param uniqueId optional argument - * @param calls the child calls on the object - */ constructor( variableIdentifier: string, typeIdentifier: string, @@ -56,18 +50,17 @@ export class ConstantObject extends ActionStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): ConstantObject { - // if (prng.nextBoolean(sampler.resampleGeneProbability)) { - // return sampler.sampleConstantObject(depth); - // } - - return new ConstantObject( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId(), - this.export - ); + // delta mutations are non existance here so we make a copy instead + return prng.nextBoolean(sampler.deltaMutationProbability) + ? this.copy() + : sampler.constantObjectGenerator.generate( + depth, + this.variableIdentifier, + this.typeIdentifier, + this.export.id, + this.name, + sampler.statementPool + ); } copy(): ConstantObject { diff --git a/libraries/search-javascript/lib/testcase/statements/action/ConstructorCall.ts b/libraries/search-javascript/lib/testcase/statements/action/ConstructorCall.ts index da6d7064b..7764af5c7 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/ConstructorCall.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/ConstructorCall.ts @@ -75,31 +75,34 @@ export class ConstructorCall extends ActionStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): ConstructorCall { - // if (prng.nextBoolean(sampler.resampleGeneProbability)) { - // return sampler.sampleConstructorCall(depth, this._classIdentifier); - // } - - const arguments_ = this.args.map((a: Statement) => a.copy()); - - if (arguments_.length > 0) { - // go over each arg - for (let index = 0; index < arguments_.length; index++) { - if (prng.nextBoolean(1 / arguments_.length)) { - arguments_[index] = arguments_[index].mutate(sampler, depth + 1); - } + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + const arguments_ = this.args.map((a: Statement) => a.copy()); + + if (arguments_.length > 0) { + const index = prng.nextInt(0, arguments_.length - 1); + arguments_[index] = arguments_[index].mutate(sampler, depth + 1); } - } - return new ConstructorCall( - this.variableIdentifier, - this.typeIdentifier, - this._classIdentifier, - this.name, - this.type, - prng.uniqueId(), - arguments_, - this.export - ); + return new ConstructorCall( + this.variableIdentifier, + this.typeIdentifier, + this._classIdentifier, + this.name, + this.type, + prng.uniqueId(), + arguments_, + this.export + ); + } else { + return sampler.constructorCallGenerator.generate( + depth, + this.variableIdentifier, + this.typeIdentifier, + this.export.id, + this.name, + sampler.statementPool + ); + } } copy(): ConstructorCall { diff --git a/libraries/search-javascript/lib/testcase/statements/action/FunctionCall.ts b/libraries/search-javascript/lib/testcase/statements/action/FunctionCall.ts index a221783d2..f9f1c38d0 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/FunctionCall.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/FunctionCall.ts @@ -61,12 +61,8 @@ export class FunctionCall extends ActionStatement { const arguments_ = this.args.map((a: Statement) => a.copy()); if (arguments_.length > 0) { - // go over each arg - for (let index = 0; index < arguments_.length; index++) { - if (prng.nextBoolean(1 / arguments_.length)) { - arguments_[index] = arguments_[index].mutate(sampler, depth + 1); - } - } + const index = prng.nextInt(0, arguments_.length - 1); + arguments_[index] = arguments_[index].mutate(sampler, depth + 1); } return new FunctionCall( @@ -105,7 +101,7 @@ export class FunctionCall extends ActionStatement { a.decode(decoder, id, options) ); - let decoded = `const ${this.varName} = await ${this._export.name}(${arguments_})`; + let decoded = `const ${this.varName} = await ${this.name}(${arguments_})`; if (options.addLogs) { const logDirectory = decoder.getLogDirectory(id, this.varName); diff --git a/libraries/search-javascript/lib/testcase/statements/action/Getter.ts b/libraries/search-javascript/lib/testcase/statements/action/Getter.ts index 34d701a97..e21630619 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/Getter.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/Getter.ts @@ -22,8 +22,6 @@ import { JavaScriptDecoder } from "../../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSampler"; import { Decoding } from "../Statement"; -import { MethodCall } from "./MethodCall"; -import { Setter } from "./Setter"; import { ClassActionStatement } from "./ClassActionStatement"; import { ConstructorCall } from "./ConstructorCall"; @@ -58,14 +56,7 @@ export class Getter extends ClassActionStatement { this._classType = "Getter"; } - mutate( - sampler: JavaScriptTestCaseSampler, - depth: number - ): Getter | Setter | MethodCall { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleClassAction(depth); - } - + mutate(sampler: JavaScriptTestCaseSampler, depth: number): Getter { const constructor_ = this.constructor_.mutate(sampler, depth + 1); return new Getter( diff --git a/libraries/search-javascript/lib/testcase/statements/action/MethodCall.ts b/libraries/search-javascript/lib/testcase/statements/action/MethodCall.ts index 24f9d65ac..6aff30f6d 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/MethodCall.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/MethodCall.ts @@ -22,8 +22,6 @@ import { JavaScriptDecoder } from "../../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSampler"; import { Decoding, Statement } from "../Statement"; -import { Getter } from "./Getter"; -import { Setter } from "./Setter"; import { ConstructorCall } from "./ConstructorCall"; import { ClassActionStatement } from "./ClassActionStatement"; @@ -60,31 +58,18 @@ export class MethodCall extends ClassActionStatement { this._classType = "MethodCall"; } - mutate( - sampler: JavaScriptTestCaseSampler, - depth: number - ): Getter | Setter | MethodCall { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleClassAction(depth); - } - - const probability = 1 / (this.args.length + 1); // plus one for the constructor - + mutate(sampler: JavaScriptTestCaseSampler, depth: number): MethodCall { const arguments_ = this.args.map((a: Statement) => a.copy()); + let constructor_ = this.constructor_.copy(); + const index = prng.nextInt(0, arguments_.length); - if (arguments_.length > 0) { + if (index < arguments_.length) { // go over each arg - for (let index = 0; index < arguments_.length; index++) { - if (prng.nextBoolean(probability)) { - arguments_[index] = arguments_[index].mutate(sampler, depth + 1); - } - } + arguments_[index] = arguments_[index].mutate(sampler, depth + 1); + } else { + constructor_ = constructor_.mutate(sampler, depth + 1); } - const constructor_ = prng.nextBoolean(probability) - ? this.constructor_.mutate(sampler, depth + 1) - : this.constructor_.copy(); - return new MethodCall( this.variableIdentifier, this.typeIdentifier, diff --git a/libraries/search-javascript/lib/testcase/statements/action/ObjectFunctionCall.ts b/libraries/search-javascript/lib/testcase/statements/action/ObjectFunctionCall.ts index bf9ef1ea8..9afcfa100 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/ObjectFunctionCall.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/ObjectFunctionCall.ts @@ -17,6 +17,7 @@ */ import { prng } from "@syntest/prng"; +import { shouldNeverHappen } from "@syntest/search"; import { JavaScriptDecoder } from "../../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSampler"; @@ -57,27 +58,17 @@ export class ObjectFunctionCall extends ActionStatement { sampler: JavaScriptTestCaseSampler, depth: number ): ObjectFunctionCall { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleObjectFunctionCall(depth); - } - - const probability = 1 / (this.args.length + 1); // plus one for the constructor - const arguments_ = this.args.map((a: Statement) => a.copy()); + let object_ = this._object.copy(); + const index = prng.nextInt(0, arguments_.length); - if (arguments_.length > 0) { + if (index < arguments_.length) { // go over each arg - for (let index = 0; index < arguments_.length; index++) { - if (prng.nextBoolean(probability)) { - arguments_[index] = arguments_[index].mutate(sampler, depth + 1); - } - } + arguments_[index] = arguments_[index].mutate(sampler, depth + 1); + } else { + object_ = object_.mutate(sampler, depth + 1); } - const object_ = prng.nextBoolean(probability) - ? this._object.mutate(sampler, depth + 1) - : this._object.copy(); - return new ObjectFunctionCall( this.variableIdentifier, this.typeIdentifier, @@ -89,6 +80,33 @@ export class ObjectFunctionCall extends ActionStatement { ); } + override setChild(index: number, newChild: Statement) { + if (!newChild) { + throw new Error("Invalid new child!"); + } + + if (index < 0 || index > this.args.length) { + throw new Error(shouldNeverHappen(`Invalid index used index: ${index}`)); + } + + if (index === this.args.length) { + if (!(newChild instanceof ConstantObject)) { + throw new TypeError(shouldNeverHappen("should be a constant object")); + } + this._object = newChild; + } else { + this.args[index] = newChild; + } + } + + override hasChildren(): boolean { + return true; + } + + override getChildren(): Statement[] { + return [...this.args, this._object]; + } + copy(): ObjectFunctionCall { const deepCopyArguments = this.args.map((a: Statement) => a.copy()); diff --git a/libraries/search-javascript/lib/testcase/statements/action/Setter.ts b/libraries/search-javascript/lib/testcase/statements/action/Setter.ts index c6807d8e5..bdef00e40 100644 --- a/libraries/search-javascript/lib/testcase/statements/action/Setter.ts +++ b/libraries/search-javascript/lib/testcase/statements/action/Setter.ts @@ -64,14 +64,10 @@ export class Setter extends ClassActionStatement { sampler: JavaScriptTestCaseSampler, depth: number ): Getter | Setter | MethodCall { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleClassAction(depth); - } - const probability = 1 / 2; // plus one for the constructor - let argument = this.args.map((a: Statement) => a.copy())[0]; let constructor_ = this.constructor_.copy(); - if (prng.nextBoolean(probability)) { + + if (prng.nextBoolean(0.5)) { argument = argument.mutate(sampler, depth + 1); } else { constructor_ = constructor_.mutate(sampler, depth + 1); diff --git a/libraries/search-javascript/lib/testcase/statements/complex/ArrayStatement.ts b/libraries/search-javascript/lib/testcase/statements/complex/ArrayStatement.ts index 7d38f28fe..8f59cffef 100644 --- a/libraries/search-javascript/lib/testcase/statements/complex/ArrayStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/complex/ArrayStatement.ts @@ -17,6 +17,7 @@ */ import { prng } from "@syntest/prng"; +import { shouldNeverHappen } from "@syntest/search"; import { JavaScriptDecoder } from "../../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSampler"; @@ -40,32 +41,50 @@ export class ArrayStatement extends Statement { super(variableIdentifier, typeIdentifier, name, type, uniqueId); this._children = children; this._classType = "ArrayStatement"; + + // check for circular + for (const [index, statement] of this._children.entries()) { + if (statement && statement.uniqueId === this.uniqueId) { + console.log("circular detected"); + this._children.splice(index, 1); + } + } } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleArgument(depth, this.variableIdentifier, this.name); - } - const children = this._children.map((a: Statement) => a.copy()); - - // - // if (children.length !== 0) { - // const index = prng.nextInt(0, children.length - 1); - // if (prng.nextBoolean(Properties.resample_gene_probability)) { // TODO should be different property - // children[index] = sampler.sampleArgument(depth + 1, children[index].identifierDescription) - // } else { - // children[index] = children[index].mutate(sampler, depth + 1); - // } - // } - - const finalChildren = []; - - // If there are no children, add one - if (children.length === 0) { - // add a item - finalChildren.push( - sampler.sampleArrayArgument(depth + 1, this.variableIdentifier, 0) - ); + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + const children = this._children.map((a: Statement) => a.copy()); + + const choice = prng.nextDouble(); + + if (children.length > 0) { + if (choice < 0.33) { + // 33% chance to add a child on this position + const index = prng.nextInt(0, children.length); + children.splice( + index, + 0, + sampler.sampleArrayArgument(depth + 1, this.typeIdentifier, index) + ); + } else if (choice < 0.66) { + // 33% chance to remove a child on this position + const index = prng.nextInt(0, children.length - 1); + children.splice(index, 1); + } else { + // 33% chance to mutate a child on this position + const index = prng.nextInt(0, children.length - 1); + children.splice( + index, + 1, + sampler.sampleArrayArgument(depth + 1, this.typeIdentifier, index) + ); + } + } else { + // no children found so we always add + children.push( + sampler.sampleArrayArgument(depth + 1, this.typeIdentifier, 0) + ); + } return new ArrayStatement( this.variableIdentifier, @@ -73,43 +92,26 @@ export class ArrayStatement extends Statement { this.name, this.type, prng.uniqueId(), - finalChildren + children ); - } - - // go over each call - for (let index = 0; index < children.length; index++) { - if (prng.nextBoolean(1 / children.length)) { - // Mutate this position - const choice = prng.nextDouble(); - - if (choice < 0.1) { - // 10% chance to add a argument on this position - finalChildren.push( - sampler.sampleArrayArgument( - depth + 1, - this.variableIdentifier, - index - ), - children[index] - ); - } else if (choice < 0.2) { - // 10% chance to delete the child - } else { - // 80% chance to just mutate the child - finalChildren.push(children[index].mutate(sampler, depth + 1)); - } + } else { + if (prng.nextBoolean(0.5)) { + // 50% + return sampler.sampleArgument( + depth, + this.variableIdentifier, + this.name + ); + } else { + // 50% + return sampler.sampleArray( + depth, + this.variableIdentifier, + this.name, + this.type + ); } } - - return new ArrayStatement( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId(), - finalChildren - ); } copy(): ArrayStatement { @@ -119,7 +121,15 @@ export class ArrayStatement extends Statement { this.name, this.type, this.uniqueId, - this._children.map((a) => a.copy()) + this._children + .filter((a) => { + if (a.uniqueId === this.uniqueId) { + console.log("circular detected"); + return false; + } + return true; + }) + .map((a) => a.copy()) ); } @@ -163,14 +173,14 @@ export class ArrayStatement extends Statement { throw new Error("Invalid new child!"); } - if (index >= this.children.length) { - throw new Error("Invalid child location!"); + if (index < 0 || index >= this.children.length) { + throw new Error(shouldNeverHappen(`Invalid index used index: ${index}`)); } this.children[index] = newChild; } - get children(): Statement[] { + protected get children(): Statement[] { return this._children; } } diff --git a/libraries/search-javascript/lib/testcase/statements/complex/ArrowFunctionStatement.ts b/libraries/search-javascript/lib/testcase/statements/complex/ArrowFunctionStatement.ts index e9c512a48..32b1023e4 100644 --- a/libraries/search-javascript/lib/testcase/statements/complex/ArrowFunctionStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/complex/ArrowFunctionStatement.ts @@ -16,9 +16,8 @@ * limitations under the License. */ -// TODO - import { prng } from "@syntest/prng"; +import { shouldNeverHappen } from "@syntest/search"; import { JavaScriptDecoder } from "../../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSampler"; @@ -47,21 +46,38 @@ export class ArrowFunctionStatement extends Statement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleArgument(depth, this.variableIdentifier, this.name); + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + return new ArrowFunctionStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId(), + this._parameters, + this.returnValue + ? this._returnValue.mutate(sampler, depth + 1) + : undefined + ); + } else { + // 20% + if (prng.nextBoolean(0.5)) { + // 50% + return sampler.sampleArgument( + depth, + this.variableIdentifier, + this.name + ); + } else { + // 50% + return sampler.sampleArrowFunction( + depth, + this.variableIdentifier, + this.name, + this.type + ); + } } - - return new ArrowFunctionStatement( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId(), - this._parameters, - this.returnValue - ? this._returnValue.mutate(sampler, depth + 1) - : undefined - ); } copy(): ArrowFunctionStatement { @@ -115,10 +131,18 @@ export class ArrowFunctionStatement extends Statement { } hasChildren(): boolean { - return true; + return this._returnValue !== undefined; } setChild(index: number, newChild: Statement) { + if (!newChild) { + throw new Error("Invalid new child!"); + } + + if (index !== 0) { + throw new Error(shouldNeverHappen(`Invalid index used index: ${index}`)); + } + this._returnValue = newChild; } diff --git a/libraries/search-javascript/lib/testcase/statements/complex/ObjectStatement.ts b/libraries/search-javascript/lib/testcase/statements/complex/ObjectStatement.ts index 5f7fc10ac..48b95b971 100644 --- a/libraries/search-javascript/lib/testcase/statements/complex/ObjectStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/complex/ObjectStatement.ts @@ -17,6 +17,7 @@ */ import { prng } from "@syntest/prng"; +import { shouldNeverHappen } from "@syntest/search"; import { JavaScriptDecoder } from "../../../testbuilding/JavaScriptDecoder"; import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSampler"; @@ -42,59 +43,118 @@ export class ObjectStatement extends Statement { super(variableIdentifier, typeIdentifier, name, type, uniqueId); this._object = object; this._classType = "ObjectStatement"; - } - mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleArgument(depth, this.variableIdentifier, this.name); + // check for circular + for (const [key, statement] of Object.entries(this._object)) { + if (statement && statement.uniqueId === this.uniqueId) { + console.log("circular detected"); + this._object[key] = undefined; + } } + } - const object: ObjectType = {}; - - for (const key of Object.keys(this._object)) { - object[key] = this._object[key].copy(); - } + mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + const object: ObjectType = {}; + + const keys = Object.keys(this._object); + + if (keys.length === 0) { + return new ObjectStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId(), + object + ); + } - const keys = Object.keys(object); - // go over each child - for (let index = 0; index < keys.length; index++) { - if (prng.nextBoolean(1 / keys.length)) { - const key = keys[index]; - // Mutate this position - const choice = prng.nextDouble(); + const availableKeys = []; + for (const key of keys) { + if (!this._object[key]) { + object[key] = undefined; + continue; + } + object[key] = this._object[key].copy(); + availableKeys.push(key); + } - if (choice < 0.1) { - // 10% chance to add a call on this position + const choice = prng.nextDouble(); + if (availableKeys.length > 0) { + if (choice < 0.33) { + // 33% chance to add a child on this position + const index = prng.nextInt(0, keys.length - 1); + const key = keys[index]; object[key] = sampler.sampleObjectArgument( depth + 1, - this.variableIdentifier, + this.typeIdentifier, key ); - } else if (choice < 0.2) { - // 10% chance to delete the call + } else if (choice < 0.66) { + // 33% chance to remove a child on this position + const key = prng.pickOne(availableKeys); object[key] = undefined; } else { - // 80% chance to just mutate the call + // 33% chance to mutate a child + const key = prng.pickOne(availableKeys); object[key] = object[key].mutate(sampler, depth + 1); } + } else { + // no keys available so we add one + const index = prng.nextInt(0, keys.length - 1); + const key = keys[index]; + object[key] = sampler.sampleObjectArgument( + depth + 1, + this.typeIdentifier, + key + ); } - } - return new ObjectStatement( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId(), - object - ); + return new ObjectStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId(), + object + ); + } else { + // 20% + if (prng.nextBoolean(0.5)) { + // 50% + return sampler.sampleArgument( + depth, + this.variableIdentifier, + this.name + ); + } else { + // 50% + return sampler.sampleObject( + depth, + this.variableIdentifier, + this.name, + this.type + ); + } + } } copy(): ObjectStatement { const object: ObjectType = {}; for (const key of Object.keys(this._object)) { + if (this._object[key] === undefined) { + object[key] = undefined; + continue; + } + if (this._object[key].uniqueId === this.uniqueId) { + console.log("circular detected"); + object[key] = undefined; + continue; + } object[key] = this._object[key].copy(); } @@ -115,14 +175,14 @@ export class ObjectStatement extends Statement { ): Decoding[] { const children = Object.keys(this._object) .filter((key) => this._object[key] !== undefined) - .map((key) => `\t"${key}": ${this._object[key].varName}`) - .join(",\n\t"); + .map((key) => `\t\t\t"${key}": ${this._object[key].varName}`) + .join(",\n"); - const childStatements: Decoding[] = Object.values(this._object).flatMap( - (a) => a.decode(decoder, id, options) - ); + const childStatements: Decoding[] = Object.values(this._object) + .filter((a) => a !== undefined) + .flatMap((a) => a.decode(decoder, id, options)); - let decoded = `const ${this.varName} = {\n${children}\n\t}`; + let decoded = `const ${this.varName} = {\n${children}\n\t\t}`; if (options.addLogs) { const logDirectory = decoder.getLogDirectory(id, this.varName); @@ -139,13 +199,14 @@ export class ObjectStatement extends Statement { } getChildren(): Statement[] { - return [...this.children]; + return Object.keys(this._object) + .sort() + .filter((key) => this._object[key] !== undefined) + .map((key) => this._object[key]); } hasChildren(): boolean { - return Object.keys(this._object).some( - (key) => this._object[key] !== undefined - ); + return this.getChildren().length > 0; } setChild(index: number, newChild: Statement) { @@ -153,16 +214,15 @@ export class ObjectStatement extends Statement { throw new Error("Invalid new child!"); } - if (index >= this.children.length) { - throw new Error("Invalid child location!"); + if (index < 0 || index >= this.getChildren().length) { + throw new Error(shouldNeverHappen(`Invalid index used index: ${index}`)); } - this.children[index] = newChild; - } + const keys = Object.keys(this._object) + .sort() + .filter((key) => this._object[key] !== undefined); + const key = keys[index]; - get children(): Statement[] { - return Object.keys(this._object) - .filter((key) => this._object[key] !== undefined) - .map((key) => this._object[key]); + this._object[key] = newChild; } } diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/BoolStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/BoolStatement.ts index 0e25c085a..c16edc8e2 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/BoolStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/BoolStatement.ts @@ -40,22 +40,24 @@ export class BoolStatement extends PrimitiveStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + return new BoolStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId(), + !this.value + ); + } else { + // 20% return sampler.sampleArgument( depth + 1, this.variableIdentifier, this.name ); } - - return new BoolStatement( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId(), - !this.value - ); } copy(): BoolStatement { diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/IntegerStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/IntegerStatement.ts index d9eb40b28..adc223158 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/IntegerStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/IntegerStatement.ts @@ -22,6 +22,7 @@ import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSamp import { PrimitiveStatement } from "./PrimitiveStatement"; import { Statement } from "../Statement"; +import { NumericStatement } from "./NumericStatement"; /** * Generic number class @@ -49,24 +50,39 @@ export class IntegerStatement extends PrimitiveStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleArgument( - depth + 1, - this.variableIdentifier, - this.name - ); - } - if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + if (prng.nextBoolean(0.5)) { + // 50% + return new NumericStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId(), + this.value + ).deltaMutation(sampler); + } return this.deltaMutation(sampler); + } else { + // 20% + if (prng.nextBoolean(0.5)) { + // 50% + return sampler.sampleArgument( + depth + 1, + this.variableIdentifier, + this.name + ); + } else { + // 50% + return sampler.sampleInteger(this.variableIdentifier, this.name); + } } - - return sampler.sampleInteger(this.variableIdentifier, this.name); } deltaMutation(sampler: JavaScriptTestCaseSampler): IntegerStatement { // small mutation - const change = prng.nextGaussian(0, 20); + const change = prng.nextGaussian(0, 5); let newValue = Math.round(this.value + change); @@ -98,7 +114,7 @@ export class IntegerStatement extends PrimitiveStatement { this.typeIdentifier, this.name, this.type, - prng.uniqueId(), + this.uniqueId, this.value ); } diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/NullStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/NullStatement.ts index 4dc4d10e5..a84ef1655 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/NullStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/NullStatement.ts @@ -39,21 +39,23 @@ export class NullStatement extends PrimitiveStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + return new NullStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId() + ); + } else { + // 20% return sampler.sampleArgument( depth + 1, this.variableIdentifier, this.name ); } - - return new NullStatement( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId() - ); } copy(): NullStatement { diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/NumericStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/NumericStatement.ts index 2c7e5effd..523e2827c 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/NumericStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/NumericStatement.ts @@ -22,6 +22,7 @@ import { JavaScriptTestCaseSampler } from "../../sampling/JavaScriptTestCaseSamp import { PrimitiveStatement } from "./PrimitiveStatement"; import { Statement } from "../Statement"; +import { IntegerStatement } from "./IntegerStatement"; /** * Generic number class @@ -42,24 +43,39 @@ export class NumericStatement extends PrimitiveStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleArgument( - depth + 1, - this.variableIdentifier, - this.name - ); - } - if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + if (prng.nextBoolean(0.5)) { + // 50% + return new IntegerStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId(), + this.value + ).deltaMutation(sampler); + } return this.deltaMutation(sampler); + } else { + // 20% + if (prng.nextBoolean(0.5)) { + // 50% + return sampler.sampleArgument( + depth + 1, + this.variableIdentifier, + this.name + ); + } else { + // 50% + return sampler.sampleNumber(this.variableIdentifier, this.name); + } } - - return sampler.sampleNumber(this.variableIdentifier, this.name); } deltaMutation(sampler: JavaScriptTestCaseSampler): NumericStatement { // small mutation - const change = prng.nextGaussian(0, 20); + const change = prng.nextGaussian(0, 5); let newValue = this.value + change; @@ -91,7 +107,7 @@ export class NumericStatement extends PrimitiveStatement { this.typeIdentifier, this.name, this.type, - prng.uniqueId(), + this.uniqueId, this.value ); } diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/PrimitiveStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/PrimitiveStatement.ts index a90306c0b..a5c2c6f89 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/PrimitiveStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/PrimitiveStatement.ts @@ -55,6 +55,10 @@ export abstract class PrimitiveStatement extends Statement { return []; } + setChild(index: number, newChild: Statement): void { + throw new Error("Primitive statements don't have children"); + } + static getRandom() { throw new Error("Unimplemented function!"); } diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/StringStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/StringStatement.ts index 123819bcb..a20e95e33 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/StringStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/StringStatement.ts @@ -48,60 +48,68 @@ export class StringStatement extends PrimitiveStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { - return sampler.sampleArgument( - depth + 1, - this.variableIdentifier, - this.name - ); - } - - if (this.value.length > 0 && this.value.length < this.maxlength) { - const value = prng.nextInt(0, 3); - - switch (value) { - case 0: { - return this.addMutation(); + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + if (this.value.length > 0 && this.value.length < this.maxlength) { + const value = prng.nextInt(0, 3); + + switch (value) { + case 0: { + // 25% + return this.addMutation(); + } + case 1: { + // 25% + return this.removeMutation(); + } + case 2: { + // 25% + return this.replaceMutation(); + } + default: { + // 25% + return this.deltaMutation(); + } } - case 1: { + } else if (this.value.length > 0) { + const value = prng.nextInt(0, 2); + + if (value === 0) { + // 33% return this.removeMutation(); - } - case 2: { + } else if (value === 1) { + // 33% return this.replaceMutation(); - } - default: { + } else { + // 33% return this.deltaMutation(); } - } - } else if (this.value.length > 0) { - const value = prng.nextInt(0, 2); - - if (value === 0) { - return this.removeMutation(); - } else if (value === 1) { - return this.replaceMutation(); } else { - return this.deltaMutation(); + // 100% + return this.addMutation(); } } else { - return this.addMutation(); + // 20% + if (prng.nextBoolean(0.5)) { + // 50% + return sampler.sampleArgument( + depth + 1, + this.variableIdentifier, + this.name + ); + } else { + // 50% + return sampler.sampleString(this.variableIdentifier, this.name); + } } } addMutation(): StringStatement { - const position = prng.nextInt(0, this.value.length - 1); + const position = prng.nextInt(0, this.value.length); const addedChar = prng.pickOne([...this.alphabet]); - let newValue = ""; - - for (let index = 0; index < this.value.length; index++) { - if (index < position || index > position) { - newValue += this.value[index]; - } else { - newValue += addedChar; - newValue += this.value[index]; - } - } + const newValue = [...this.value]; + newValue.splice(position, 0, addedChar); return new StringStatement( this.variableIdentifier, @@ -109,7 +117,7 @@ export class StringStatement extends PrimitiveStatement { this.name, this.type, prng.uniqueId(), - newValue, + newValue.join(""), this.alphabet, this.maxlength ); @@ -118,14 +126,8 @@ export class StringStatement extends PrimitiveStatement { removeMutation(): StringStatement { const position = prng.nextInt(0, this.value.length - 1); - let newValue = ""; - - for (let index = 0; index < this.value.length; index++) { - if (index === position) { - continue; - } - newValue += this.value[index]; - } + const newValue = [...this.value]; + newValue.splice(position, 1); return new StringStatement( this.variableIdentifier, @@ -133,7 +135,7 @@ export class StringStatement extends PrimitiveStatement { this.name, this.type, prng.uniqueId(), - newValue, + newValue.join(""), this.alphabet, this.maxlength ); @@ -143,12 +145,8 @@ export class StringStatement extends PrimitiveStatement { const position = prng.nextInt(0, this.value.length - 1); const newChar = prng.pickOne([...this.alphabet]); - let newValue = ""; - - for (let index = 0; index < this.value.length; index++) { - newValue += - index < position || index > position ? this.value[index] : newChar; - } + const newValue = [...this.value]; + newValue.splice(position, 1, newChar); return new StringStatement( this.variableIdentifier, @@ -156,7 +154,7 @@ export class StringStatement extends PrimitiveStatement { this.name, this.type, prng.uniqueId(), - newValue, + newValue.join(""), this.alphabet, this.maxlength ); @@ -166,16 +164,21 @@ export class StringStatement extends PrimitiveStatement { const position = prng.nextInt(0, this.value.length - 1); const oldChar = this.value[position]; const indexOldChar = this.alphabet.indexOf(oldChar); - const delta = prng.pickOne([-2, -1, 1, -2]); - const newChar = - this.alphabet[(indexOldChar + delta) % this.alphabet.length]; - - let newValue = ""; + let delta = Number(prng.nextGaussian(0, 3).toFixed(0)); + if (delta === 0) { + delta = prng.nextBoolean() ? 1 : -1; + } - for (let index = 0; index < this.value.length; index++) { - newValue += - index < position || index > position ? this.value[index] : newChar; + let newIndex = indexOldChar + delta; + if (newIndex < 0) { + newIndex = this.alphabet.length + newIndex; } + newIndex = newIndex % this.alphabet.length; + // const delta = prng.pickOne([-2, -1, 1, -2]); + const newChar = this.alphabet[newIndex]; + + const newValue = [...this.value]; + newValue.splice(position, 1, newChar); return new StringStatement( this.variableIdentifier, @@ -183,7 +186,7 @@ export class StringStatement extends PrimitiveStatement { this.name, this.type, prng.uniqueId(), - newValue, + newValue.join(""), this.alphabet, this.maxlength ); @@ -204,10 +207,13 @@ export class StringStatement extends PrimitiveStatement { override decode(): Decoding[] { let value = this.value; - value = value.replace(/\n/g, "\\n"); - value = value.replace(/\r/g, "\\r"); - value = value.replace(/\t/g, "\\t"); - value = value.replace(/"/g, '\\"'); + + value = value.replaceAll(/\\/g, "\\\\"); + value = value.replaceAll(/\n/g, "\\n"); + value = value.replaceAll(/\r/g, "\\r"); + value = value.replaceAll(/\t/g, "\\t"); + value = value.replaceAll(/"/g, '\\"'); + return [ { decoded: `const ${this.varName} = "${value}";`, diff --git a/libraries/search-javascript/lib/testcase/statements/primitive/UndefinedStatement.ts b/libraries/search-javascript/lib/testcase/statements/primitive/UndefinedStatement.ts index a8728e00a..f853ef265 100644 --- a/libraries/search-javascript/lib/testcase/statements/primitive/UndefinedStatement.ts +++ b/libraries/search-javascript/lib/testcase/statements/primitive/UndefinedStatement.ts @@ -46,20 +46,23 @@ export class UndefinedStatement extends PrimitiveStatement { } mutate(sampler: JavaScriptTestCaseSampler, depth: number): Statement { - if (prng.nextBoolean(sampler.resampleGeneProbability)) { + if (prng.nextBoolean(sampler.deltaMutationProbability)) { + // 80% + return new UndefinedStatement( + this.variableIdentifier, + this.typeIdentifier, + this.name, + this.type, + prng.uniqueId() + ); + } else { + // 20% return sampler.sampleArgument( depth + 1, this.variableIdentifier, this.name ); } - return new UndefinedStatement( - this.variableIdentifier, - this.typeIdentifier, - this.name, - this.type, - prng.uniqueId() - ); } copy(): UndefinedStatement { diff --git a/libraries/search-javascript/package.json b/libraries/search-javascript/package.json index e7716306f..e1b554c47 100644 --- a/libraries/search-javascript/package.json +++ b/libraries/search-javascript/package.json @@ -65,6 +65,7 @@ "clear": "0.1.0", "figlet": "1.5.2", "fs-extra": "11.1.0", + "isolated-vm": "^4.6.0", "istanbul-lib-instrument": "5.1.0", "lodash.clonedeep": "4.5.0", "mocha": "10.2.0", diff --git a/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleEqual.test.ts b/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleEqual.test.ts index 45362b0dc..b4ec0a9a5 100644 --- a/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleEqual.test.ts +++ b/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleEqual.test.ts @@ -144,7 +144,7 @@ describe("BranchDistance a == b test", () => { expect( calculator.calculate("", condition, variables, trueOrFalse) - ).to.be.closeTo(0.333_33, 0.1); + ).to.be.equal(0.5); }); it("'a' == 'b' false", () => { diff --git a/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleNotEqual.test.ts b/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleNotEqual.test.ts index 8ffc45a26..ffa8048ae 100644 --- a/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleNotEqual.test.ts +++ b/libraries/search-javascript/test/criterion/BranchDistanceBinaryDoubleNotEqual.test.ts @@ -158,7 +158,7 @@ describe("BranchDistance a != b test", () => { expect( calculator.calculate("", condition, variables, trueOrFalse) - ).to.be.closeTo(0.333_33, 0.1); + ).to.be.equal(0.5); }); // number string mix diff --git a/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleEqual.test.ts b/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleEqual.test.ts index 760967f9c..78110a18d 100644 --- a/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleEqual.test.ts +++ b/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleEqual.test.ts @@ -144,7 +144,7 @@ describe("BranchDistance a === b test", () => { expect( calculator.calculate("", condition, variables, trueOrFalse) - ).to.be.closeTo(0.3333, 0.1); + ).to.be.equal(0.5); }); it("'a' === 'b' false", () => { @@ -189,4 +189,34 @@ describe("BranchDistance a === b test", () => { calculator.calculate("", condition, variables, trueOrFalse) ).to.equal(0); }); + + it("typeof a === 'string' true", () => { + const condition = "typeof a === 'string'"; + const variables = { + a: 1, + }; + const trueOrFalse = true; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.approximately(0.999, 0.001); + }); + + it("typeof 1 === 'string' true", () => { + const condition = "typeof 1 === 'string'"; + const variables = {}; + const trueOrFalse = true; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.approximately(0.999, 0.001); + }); }); diff --git a/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleNotEqual.test.ts b/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleNotEqual.test.ts index 7181ddcab..d64f2698e 100644 --- a/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleNotEqual.test.ts +++ b/libraries/search-javascript/test/criterion/BranchDistanceBinaryTrippleNotEqual.test.ts @@ -158,7 +158,7 @@ describe("BranchDistance a !== b test", () => { expect( calculator.calculate("", condition, variables, trueOrFalse) - ).to.be.closeTo(0.3333, 0.1); + ).to.be.equal(0.5); }); // number string mix diff --git a/libraries/search-javascript/test/criterion/RandomBranchDistanceTests.test.ts b/libraries/search-javascript/test/criterion/RandomBranchDistanceTests.test.ts new file mode 100644 index 000000000..686ebfdf9 --- /dev/null +++ b/libraries/search-javascript/test/criterion/RandomBranchDistanceTests.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2020-2023 Delft University of Technology and SynTest contributors + * + * This file is part of SynTest Framework - SynTest JavaScript. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect } from "chai"; +import { BranchDistance } from "../../lib/criterion/BranchDistance"; + +describe("Random Tests", () => { + it("a !== undefined && !b true", () => { + const condition = "a !== undefined && !b"; + const variables = { + b: 1, + }; + const trueOrFalse = true; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.equal(0.5); + }); + + it("args false", () => { + const condition = "args"; + const variables = { + args: "\n", + }; + const trueOrFalse = true; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.equal(0); + }); + + it("args false", () => { + const condition = "args"; + const variables = { + args: "\n", + }; + const trueOrFalse = false; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.equal(0.5); + }); + + it("cmd.options.length true", () => { + const condition = "args"; + const variables = { + "cmd.options.length": 0, + }; + const trueOrFalse = true; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.equal(0.5); + }); + + it("cmd.options.length false", () => { + const condition = "args"; + const variables = { + "cmd.options.length": 0, + }; + const trueOrFalse = false; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.equal(0); + }); + + it("option.defaultValue !== undefined && !option.negate true", () => { + const condition = "option.defaultValue !== undefined && !option.negate"; + const variables: { [key: string]: null } = { + // eslint-disable-next-line unicorn/no-null + "option.defaultValue": null, + }; + const trueOrFalse = true; + + const calculator = new BranchDistance( + "0123456789abcdefghijklmnopqrstuvxyz" + ); + + expect( + calculator.calculate("", condition, variables, trueOrFalse) + ).to.equal(0); + }); +}); diff --git a/package-lock.json b/package-lock.json index c0c1f53db..f1131778c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,26 +193,9 @@ "node": ">=16" } }, - "../syntest-core/libraries/storage": { - "name": "@syntest/storage", - "version": "0.1.0-beta.0", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@syntest/logging": "*", - "fs-extra": "^11.1.1", - "yargs": "^17.7.1" - }, - "devDependencies": { - "@types/fs-extra": "^11.0.1" - }, - "engines": { - "node": ">=16" - } - }, "../syntest-core/tools/base-language": { "name": "@syntest/base-language", - "version": "0.2.0-beta.50", + "version": "0.2.0-beta.52", "license": "Apache-2.0", "dependencies": { "@syntest/analysis": "^0.1.0-beta.9", @@ -247,29 +230,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "../syntest-core/tools/cli": { - "name": "@syntest/cli", - "version": "0.2.0-beta.26", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@syntest/cli-graphics": "^0.1.0-beta.3", - "@syntest/init": "^0.2.0-beta.19", - "@syntest/logging": "^0.1.0-beta.7", - "@syntest/metric": "^0.1.0-beta.6", - "@syntest/module": "^0.1.0-beta.19", - "@syntest/prng": "^0.1.0-beta.0", - "@syntest/storage": "^0.1.0-beta.0", - "short-uuid": "^4.2.2", - "yargs": "^17.7.1" - }, - "bin": { - "syntest": "dist/bin.js" - }, - "engines": { - "node": ">=16" - } - }, "libraries/analysis-javascript": { "name": "@syntest/analysis-javascript", "version": "0.1.0-beta.19", @@ -432,6 +392,7 @@ "clear": "0.1.0", "figlet": "1.5.2", "fs-extra": "11.1.0", + "isolated-vm": "^4.6.0", "istanbul-lib-instrument": "5.1.0", "lodash.clonedeep": "4.5.0", "mocha": "10.2.0", @@ -4580,8 +4541,7 @@ }, "node_modules/@syntest/storage": { "version": "0.1.0-beta.0", - "resolved": "https://registry.npmjs.org/@syntest/storage/-/storage-0.1.0-beta.0.tgz", - "integrity": "sha512-sIuFMbK5CwbEhAcsIXKEZeUschEIspW0GzHlhXeXH7ZIugznwo2wR5Tfdb3H1xhjkppXRNxSA8/5KJbP7o9U7Q==", + "license": "Apache-2.0", "dependencies": { "@syntest/logging": "*", "fs-extra": "^11.1.1", @@ -5360,7 +5320,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, "funding": [ { "type": "github", @@ -5423,7 +5382,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -5496,7 +5454,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "dev": true, "funding": [ { "type": "github", @@ -6475,6 +6432,19 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "0.7.0", "dev": true, @@ -6490,6 +6460,13 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -6711,6 +6688,13 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "dev": true, @@ -6828,7 +6812,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -7517,6 +7500,13 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "dev": true, @@ -7814,7 +7804,6 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -8154,6 +8143,10 @@ "ini": "^1.3.2" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "license": "MIT" + }, "node_modules/glob": { "version": "8.1.0", "dev": true, @@ -8514,7 +8507,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -8645,7 +8637,6 @@ }, "node_modules/ini": { "version": "1.3.8", - "dev": true, "license": "ISC" }, "node_modules/init-package-json": { @@ -9166,6 +9157,17 @@ "node": ">=0.10.0" } }, + "node_modules/isolated-vm": { + "version": "4.6.0", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "license": "BSD-3-Clause", @@ -10307,6 +10309,16 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "dev": true, @@ -10328,7 +10340,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10458,6 +10469,10 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "license": "MIT" + }, "node_modules/mkdirp-infer-owner": { "version": "2.0.0", "dev": true, @@ -10640,6 +10655,10 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -10675,6 +10694,16 @@ "path-to-regexp": "^1.7.0" } }, + "node_modules/node-abi": { + "version": "3.45.0", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "3.2.1", "dev": true, @@ -12022,6 +12051,30 @@ "node": ">=4" } }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -12124,6 +12177,14 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "dev": true, @@ -12175,6 +12236,26 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read": { "version": "1.0.7", "dev": true, @@ -12463,7 +12544,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -12855,7 +12935,6 @@ }, "node_modules/semver": { "version": "7.5.2", - "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" @@ -12869,7 +12948,6 @@ }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -12937,6 +13015,47 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "dev": true, @@ -13162,7 +13281,6 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -13393,9 +13511,22 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", - "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -13656,6 +13787,16 @@ "dev": true, "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -13877,7 +14018,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -14246,7 +14386,6 @@ }, "node_modules/yallist": { "version": "4.0.0", - "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/tools/javascript/lib/JavaScriptLauncher.ts b/tools/javascript/lib/JavaScriptLauncher.ts index d021a6e2b..f9ffb6156 100644 --- a/tools/javascript/lib/JavaScriptLauncher.ts +++ b/tools/javascript/lib/JavaScriptLauncher.ts @@ -21,7 +21,6 @@ import * as path from "node:path"; import { TestCommandOptions } from "./commands/test"; import { TypeModelFactory, - RandomTypeModelFactory, InferenceTypeModelFactory, Target, AbstractSyntaxTreeFactory, @@ -78,7 +77,6 @@ import { } from "@syntest/search"; import { Instrumenter } from "@syntest/instrumentation-javascript"; import { getLogger, Logger } from "@syntest/logging"; -import { TargetType } from "@syntest/analysis"; import { MetricManager } from "@syntest/metric"; import { StorageManager } from "@syntest/storage"; import traverse from "@babel/traverse"; @@ -96,6 +94,9 @@ export class JavaScriptLauncher extends Launcher { private coveredInPath = new Map>(); + private decoder: JavaScriptDecoder; + private runner: JavaScriptRunner; + constructor( arguments_: JavaScriptArguments, moduleManager: ModuleManager, @@ -157,10 +158,7 @@ export class JavaScriptLauncher extends Launcher { const dependencyFactory = new DependencyFactory(); const exportFactory = new ExportFactory(); const typeExtractor = new TypeExtractor(); - const typeResolver: TypeModelFactory = - (this.arguments_).typeInferenceMode === "none" - ? new RandomTypeModelFactory() - : new InferenceTypeModelFactory(); + const typeResolver: TypeModelFactory = new InferenceTypeModelFactory(); this.rootContext = new RootContext( this.arguments_.targetRootDirectory, @@ -271,18 +269,10 @@ export class JavaScriptLauncher extends Launcher { const mutationSettings: TableObject = { headers: ["Setting", "Value"], rows: [ - [ - "Resampling Probability", - `${this.arguments_.resampleGeneProbability}`, - ], [ "Delta Mutation Probability", `${this.arguments_.deltaMutationProbability}`, ], - [ - "Sample Existing Value Probability", - `${this.arguments_.sampleExistingValueProbability}`, - ], ["Crossover Probability", `${this.arguments_.crossoverProbability}`], [ "Multi-point Crossover Probability", @@ -292,10 +282,6 @@ export class JavaScriptLauncher extends Launcher { ["Max Depth", `${this.arguments_.maxDepth}`], ["Max Action Statements", `${this.arguments_.maxActionStatements}`], ["Explore Illegal Values", `${this.arguments_.exploreIllegalValues}`], - [ - "Sample Output Values", - `${this.arguments_.sampleFunctionOutputAsArgument}`, - ], [ "Use Constant Pool Values", `${(this.arguments_).constantPool}`, @@ -304,6 +290,22 @@ export class JavaScriptLauncher extends Launcher { "Use Constant Pool Probability", `${(this.arguments_).constantPoolProbability}`, ], + [ + "Use Type Pool Values", + `${(this.arguments_).typePool}`, + ], + [ + "Use Type Pool Probability", + `${(this.arguments_).typePoolProbability}`, + ], + [ + "Use Statement Pool Values", + `${(this.arguments_).statementPool}`, + ], + [ + "Use Statement Pool Probability", + `${(this.arguments_).statementPoolProbability}`, + ], ], footers: ["", ""], }; @@ -375,6 +377,27 @@ export class JavaScriptLauncher extends Launcher { PropertyName.PREPROCESS_TIME, `${timeInMs}` ); + + this.decoder = new JavaScriptDecoder( + this.arguments_.targetRootDirectory, + path.join( + this.arguments_.tempSyntestDirectory, + this.arguments_.fid, + this.arguments_.logDirectory + ) + ); + const executionInformationIntegrator = new ExecutionInformationIntegrator( + this.rootContext.getTypeModel() + ); + this.runner = new JavaScriptRunner( + this.storageManager, + this.decoder, + executionInformationIntegrator, + this.arguments_.testDirectory, + (this.arguments_).executionTimeout, + (this.arguments_).testTimeout + ); + JavaScriptLauncher.LOGGER.info("Preprocessing done"); } @@ -401,35 +424,20 @@ export class JavaScriptLauncher extends Launcher { async postprocess(): Promise { JavaScriptLauncher.LOGGER.info("Postprocessing started"); const start = Date.now(); - const decoder = new JavaScriptDecoder( - this.arguments_.targetRootDirectory, - path.join( - this.arguments_.tempSyntestDirectory, - this.arguments_.fid, - this.arguments_.logDirectory - ) - ); - - const executionInformationIntegrator = new ExecutionInformationIntegrator( - this.rootContext.getTypeModel() - ); - - const runner = new JavaScriptRunner( - this.storageManager, - decoder, - executionInformationIntegrator, - this.arguments_.testDirectory - ); const suiteBuilder = new JavaScriptSuiteBuilder( this.storageManager, - decoder, - runner, + this.decoder, + this.runner, this.arguments_.logDirectory ); const reducedArchive = suiteBuilder.reduceArchive(this.archive); + if (this.archive.size === 0) { + throw new Error("Zero tests were created"); + } + // TODO fix hardcoded paths let paths = suiteBuilder.createSuite( reducedArchive, @@ -438,7 +446,7 @@ export class JavaScriptLauncher extends Launcher { true, false ); - await suiteBuilder.runSuite(paths); + await suiteBuilder.runSuite(paths, this.archive.size); // reset states this.storageManager.clearTemporaryDirectory([ @@ -457,7 +465,10 @@ export class JavaScriptLauncher extends Launcher { false, true ); - const { stats, instrumentationData } = await suiteBuilder.runSuite(paths); + const { stats, instrumentationData } = await suiteBuilder.runSuite( + paths, + this.archive.size + ); if (stats.failures > 0) { this.userInterface.printError("Test case has failed!"); @@ -617,12 +628,6 @@ export class JavaScriptLauncher extends Launcher { const rootTargets = currentSubject .getActionableTargets() - .filter( - (target) => - target.type === TargetType.FUNCTION || - target.type === TargetType.CLASS || - target.type === TargetType.OBJECT - ) .filter((target) => isExported(target)); if (rootTargets.length === 0) { @@ -637,24 +642,6 @@ export class JavaScriptLauncher extends Launcher { const dependencyMap = new Map(); dependencyMap.set(target.name, dependencies); - const decoder = new JavaScriptDecoder( - this.arguments_.targetRootDirectory, - path.join( - this.arguments_.tempSyntestDirectory, - this.arguments_.fid, - this.arguments_.logDirectory - ) - ); - const executionInformationIntegrator = new ExecutionInformationIntegrator( - this.rootContext.getTypeModel() - ); - const runner = new JavaScriptRunner( - this.storageManager, - decoder, - executionInformationIntegrator, - this.arguments_.testDirectory - ); - JavaScriptLauncher.LOGGER.info("Extracting constants"); const constantPoolManager = new ConstantPoolManager(); const targetAbstractSyntaxTree = this.rootContext.getAbstractSyntaxTree( @@ -688,17 +675,19 @@ export class JavaScriptLauncher extends Launcher { constantPoolManager, (this.arguments_).constantPool, (this.arguments_).constantPoolProbability, + (this.arguments_).typePool, + (this.arguments_).typePoolProbability, + (this.arguments_).statementPool, + (this.arguments_).statementPoolProbability, + (this.arguments_).typeInferenceMode, (this.arguments_).randomTypeProbability, (this.arguments_).incorporateExecutionInformation, this.arguments_.maxActionStatements, this.arguments_.stringAlphabet, this.arguments_.stringMaxLength, - this.arguments_.resampleGeneProbability, this.arguments_.deltaMutationProbability, - this.arguments_.exploreIllegalValues, - (this.arguments_).reuseStatementProbability, - (this.arguments_).useMockedObjectProbability + this.arguments_.exploreIllegalValues ); sampler.rootContext = rootContext; @@ -719,7 +708,7 @@ export class JavaScriptLauncher extends Launcher { this.arguments_.objectiveManager ) )).createObjectiveManager({ - runner: runner, + runner: this.runner, secondaryObjectives: secondaryObjectives, }); @@ -787,7 +776,7 @@ export class JavaScriptLauncher extends Launcher { )).createTerminationTrigger({ objectiveManager: objectiveManager, encodingSampler: sampler, - runner: runner, + runner: this.runner, crossover: crossover, populationSize: this.arguments_.populationSize, }) @@ -839,6 +828,7 @@ export class JavaScriptLauncher extends Launcher { async exit(): Promise { JavaScriptLauncher.LOGGER.info("Exiting"); + this.runner.process.kill(); // TODO should be cleanup step in tool // Finish JavaScriptLauncher.LOGGER.info("Deleting temporary directories"); diff --git a/tools/javascript/lib/commands/test.ts b/tools/javascript/lib/commands/test.ts index f6b9bd641..a78226e1e 100644 --- a/tools/javascript/lib/commands/test.ts +++ b/tools/javascript/lib/commands/test.ts @@ -34,6 +34,7 @@ export function getTestCommand( const commandGroup = "Type Inference Options:"; const samplingGroup = "Sampling Options:"; + const executorGroup = "Test Execution Options:"; options.set("incorporate-execution-information", { alias: [], @@ -63,45 +64,82 @@ export function getTestCommand( type: "number", }); - options.set("reuse-statement-probability", { + options.set("constant-pool", { + alias: [], + default: true, + description: "Enable constant pool.", + group: samplingGroup, + hidden: false, + type: "boolean", + }); + + options.set("constant-pool-probability", { alias: [], - default: 0.9, + default: 0.5, description: - "The probability we reuse a statement instead of generating a new one.", + "Probability to sample from the constant pool instead creating random values", group: samplingGroup, hidden: false, type: "number", }); - options.set("use-mocked-object-probability", { + options.set("type-pool", { alias: [], - default: 0.1, + default: true, + description: "Enable type pool.", + group: samplingGroup, + hidden: false, + type: "boolean", + }); + + options.set("type-pool-probability", { + alias: [], + default: 0.5, description: - "The probability we use a mocked object instead of generating an actual instance.", + "Probability to sample from the type pool instead creating random values", group: samplingGroup, hidden: false, type: "number", }); - options.set("constant-pool", { + options.set("statement-pool", { alias: [], - default: false, - description: "Enable constant pool.", + default: true, + description: "Enable statement pool.", group: samplingGroup, hidden: false, type: "boolean", }); - options.set("constant-pool-probability", { + options.set("statement-pool-probability", { alias: [], - default: 0.5, + default: 0.8, description: - "Probability to sample from the constant pool instead creating random values", + "Probability to sample from the statement pool instead creating new values", group: samplingGroup, hidden: false, type: "number", }); + options.set("execution-timeout", { + alias: [], + default: 2000, + description: + "The timeout for one execution of one test (must be larger than the test-timeout).", + group: executorGroup, + hidden: false, + type: "number", + }); + + options.set("test-timeout", { + alias: [], + default: 1000, + description: "The timeout for one test.", + group: executorGroup, + hidden: false, + type: "number", + }); + return new Command( moduleManager, tool, @@ -125,8 +163,12 @@ export type TestCommandOptions = { incorporateExecutionInformation: boolean; typeInferenceMode: string; randomTypeProbability: number; - reuseStatementProbability: number; - useMockedObjectProbability: number; constantPool: boolean; constantPoolProbability: number; + typePool: boolean; + typePoolProbability: number; + statementPool: boolean; + statementPoolProbability: number; + executionTimeout: number; + testTimeout: number; }; diff --git a/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts b/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts index b6622820c..7f0a078ff 100644 --- a/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts +++ b/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts @@ -39,9 +39,13 @@ export class RandomSamplerPlugin extends SamplerPlugin { ): EncodingSampler { return new JavaScriptRandomSampler( options.subject as unknown as JavaScriptSubject, - undefined, - undefined, - undefined, + undefined, // TODO incorrect constant pool should be part of sampler options + ((this.args)).constantPool, + ((this.args)).constantPoolProbability, + ((this.args)).typePool, + ((this.args)).typePoolProbability, + ((this.args)).statementPool, + ((this.args)).statementPoolProbability, ((this.args)).typeInferenceMode, ((this.args)).randomTypeProbability, (( @@ -50,11 +54,8 @@ export class RandomSamplerPlugin extends SamplerPlugin { ((this.args)).maxActionStatements, ((this.args)).stringAlphabet, ((this.args)).stringMaxLength, - ((this.args)).resampleGeneProbability, ((this.args)).deltaMutationProbability, - ((this.args)).exploreIllegalValues, - ((this.args)).reuseStatementProbability, - ((this.args)).useMockedObjectProbability + ((this.args)).exploreIllegalValues ); }