diff --git a/libraries/analysis-javascript/index.ts b/libraries/analysis-javascript/index.ts index 533f08979..0dcfc0d69 100644 --- a/libraries/analysis-javascript/index.ts +++ b/libraries/analysis-javascript/index.ts @@ -21,6 +21,10 @@ export * from "./lib/ast/defaultBabelConfig"; export * from "./lib/cfg/ControlFlowGraphFactory"; export * from "./lib/cfg/ControlFlowGraphVisitor"; +export * from "./lib/constant/ConstantPool"; +export * from "./lib/constant/ConstantPoolManager"; +export * from "./lib/constant/ConstantVisitor"; + export * from "./lib/dependency/DependencyFactory"; export * from "./lib/dependency/DependencyVisitor"; diff --git a/libraries/analysis-javascript/lib/constant/ConstantPool.ts b/libraries/analysis-javascript/lib/constant/ConstantPool.ts new file mode 100644 index 000000000..1e7098093 --- /dev/null +++ b/libraries/analysis-javascript/lib/constant/ConstantPool.ts @@ -0,0 +1,165 @@ +/* + * 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 { prng } from "@syntest/prng"; + +export class ConstantPool { + protected _numericPool: Map; + protected _integerPool: Map; + protected _bigIntPool: Map; + protected _stringPool: Map; + protected _numericCount: number; + protected _integerCount: number; + protected _bigIntCount: number; + protected _stringCount: number; + + constructor() { + 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); + this.addInteger(0); + this.addInteger(+1); + + this._bigIntPool = new Map(); + + this._stringPool = new Map(); + this.addString(""); + } + + public addNumeric(value: number): void { + if (this._numericPool.has(value)) { + this._numericPool.set(value, this._numericPool.get(value) + 1); + } else { + this._numericPool.set(value, 1); + } + this._numericCount++; + } + + public addInteger(value: number): void { + if (this._integerPool.has(value)) { + this._integerPool.set(value, this._integerPool.get(value) + 1); + } else { + this._integerPool.set(value, 1); + } + this._integerCount++; + } + + public addBigInt(value: bigint): void { + if (this._bigIntPool.has(value)) { + this._bigIntPool.set(value, this._bigIntPool.get(value) + 1); + } else { + this._bigIntPool.set(value, 1); + } + this._bigIntCount++; + } + + public addString(value: string): void { + if (this._stringPool.has(value)) { + this._stringPool.set(value, this._stringPool.get(value) + 1); + } else { + this._stringPool.set(value, 1); + } + this._stringCount++; + } + + public getRandomNumeric(frequencyBased = false): number { + if (this._numericPool.size === 0) { + return undefined; + } + + if (frequencyBased) { + let index = prng.nextDouble() * this._numericCount; + for (const [value, frequency] of this._numericPool.entries()) { + if (index >= frequency) { + return value; + } else { + index -= frequency; + } + } + return prng.pickOne([...this._numericPool.keys()]); + } else { + return prng.pickOne([...this._numericPool.keys()]); + } + } + + public getRandomInteger(frequencyBased = false): number { + if (this._integerPool.size === 0) { + return undefined; + } + + if (frequencyBased) { + let index = prng.nextDouble() * this._integerCount; + for (const [value, frequency] of this._integerPool.entries()) { + if (index >= frequency) { + return value; + } else { + index -= frequency; + } + } + return prng.pickOne([...this._integerPool.keys()]); + } else { + return prng.pickOne([...this._integerPool.keys()]); + } + } + + public getRandomBigInt(frequencyBased = false): bigint { + if (this._bigIntPool.size === 0) { + return undefined; + } + + if (frequencyBased) { + let index = prng.nextDouble() * this._bigIntCount; + for (const [value, frequency] of this._bigIntPool.entries()) { + if (index >= frequency) { + return value; + } else { + index -= frequency; + } + } + return prng.pickOne([...this._bigIntPool.keys()]); + } else { + return prng.pickOne([...this._bigIntPool.keys()]); + } + } + + public getRandomString(frequencyBased = false): string { + if (this._stringPool.size === 0) { + return undefined; + } + + if (frequencyBased) { + let index = prng.nextDouble() * this._stringCount; + for (const [value, frequency] of this._stringPool.entries()) { + if (index >= frequency) { + return value; + } else { + index -= frequency; + } + } + return prng.pickOne([...this._stringPool.keys()]); + } else { + return prng.pickOne([...this._stringPool.keys()]); + } + } +} diff --git a/libraries/analysis-javascript/lib/constant/ConstantPoolManager.ts b/libraries/analysis-javascript/lib/constant/ConstantPoolManager.ts new file mode 100644 index 000000000..60f375301 --- /dev/null +++ b/libraries/analysis-javascript/lib/constant/ConstantPoolManager.ts @@ -0,0 +1,43 @@ +/* + * 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 { ConstantPool } from "./ConstantPool"; + +export class ConstantPoolManager { + protected _targetConstantPool: ConstantPool; + protected _contextConstantPool: ConstantPool; + protected _dynamicConstantPool: ConstantPool; + + constructor() { + this._targetConstantPool = new ConstantPool(); + this._contextConstantPool = new ConstantPool(); + this._dynamicConstantPool = new ConstantPool(); + } + + public get targetConstantPool(): ConstantPool { + return this._targetConstantPool; + } + + public get contextConstantPool(): ConstantPool { + return this._contextConstantPool; + } + + public get dynamicConstantPool(): ConstantPool { + return this._dynamicConstantPool; + } +} diff --git a/libraries/analysis-javascript/lib/constant/ConstantVisitor.ts b/libraries/analysis-javascript/lib/constant/ConstantVisitor.ts new file mode 100644 index 000000000..2598b9b8d --- /dev/null +++ b/libraries/analysis-javascript/lib/constant/ConstantVisitor.ts @@ -0,0 +1,75 @@ +/* + * 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 { NodePath } from "@babel/core"; +import * as t from "@babel/types"; +import { AbstractSyntaxTreeVisitor } from "@syntest/ast-visitor-javascript"; +import { ConstantPool } from "./ConstantPool"; + +export class ConstantVisitor extends AbstractSyntaxTreeVisitor { + protected _constantPool: ConstantPool; + + constructor(filePath: string, constantPool: ConstantPool) { + super(filePath); + this._constantPool = constantPool; + } + + public Literal: (path: NodePath) => void = ( + path: NodePath + ) => { + switch (path.node.type) { + case "StringLiteral": { + this._constantPool.addString(path.node.value); + break; + } + case "NumericLiteral": { + if (Number.isInteger(path.node.value)) { + this._constantPool.addInteger(path.node.value); + } else { + this._constantPool.addNumeric(path.node.value); + } + break; + } + case "NullLiteral": { + // Not useful for the constant pool + break; + } + case "BooleanLiteral": { + // Not useful for the constant pool + break; + } + case "RegExpLiteral": { + break; + } + case "TemplateLiteral": { + break; + } + case "BigIntLiteral": { + this._constantPool.addBigInt(BigInt(path.node.value)); + break; + } + case "DecimalLiteral": { + this._constantPool.addNumeric(Number(path.node.value)); + break; + } + default: { + // should never occur + throw new Error(`Unknown literal type`); + } + } + }; +} diff --git a/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts b/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts index ef280eb67..01bbd91a2 100644 --- a/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts +++ b/libraries/search-javascript/lib/testcase/sampling/JavaScriptRandomSampler.ts @@ -18,6 +18,7 @@ import { ClassTarget, + ConstantPoolManager, FunctionTarget, getRelationName, isExported, @@ -59,6 +60,9 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { constructor( subject: JavaScriptSubject, + constantPoolManager: ConstantPoolManager, + constantPoolEnabled: boolean, + constantPoolProbability: number, typeInferenceMode: string, randomTypeProbability: number, incorporateExecutionInformation: boolean, @@ -71,6 +75,9 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { ) { super( subject, + constantPoolManager, + constantPoolEnabled, + constantPoolProbability, typeInferenceMode, randomTypeProbability, incorporateExecutionInformation, @@ -774,11 +781,21 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { alphabet = this.stringAlphabet, maxlength = this.stringMaxLength ): StringStatement { - const valueLength = prng.nextInt(0, maxlength - 1); - let value = ""; + let value: string; + if ( + this.constantPoolEnabled && + prng.nextBoolean(this.constantPoolProbability) + ) { + value = this.constantPoolManager.contextConstantPool.getRandomString(); + } - for (let index = 0; index < valueLength; index++) { - value += prng.pickOne([...alphabet]); + if (value === undefined) { + value = ""; + const valueLength = prng.nextInt(0, maxlength - 1); + + for (let index = 0; index < valueLength; index++) { + value += prng.pickOne([...alphabet]); + } } return new StringStatement( @@ -812,12 +829,21 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { const max = 10; const min = -10; + const value = + this.constantPoolEnabled && prng.nextBoolean(this.constantPoolProbability) + ? this.constantPoolManager.contextConstantPool.getRandomNumeric() + : prng.nextDouble(min, max); + + if (value === undefined) { + prng.nextDouble(min, max); + } + return new NumericStatement( id, name, TypeEnum.NUMERIC, prng.uniqueId(), - prng.nextDouble(min, max) + value ); } @@ -826,12 +852,21 @@ export class JavaScriptRandomSampler extends JavaScriptTestCaseSampler { const max = 10; const min = -10; + const value = + this.constantPoolEnabled && prng.nextBoolean(this.constantPoolProbability) + ? this.constantPoolManager.contextConstantPool.getRandomInteger() + : prng.nextInt(min, max); + + if (value === undefined) { + prng.nextInt(min, max); + } + return new IntegerStatement( id, name, TypeEnum.INTEGER, prng.uniqueId(), - prng.nextInt(min, max) + value ); } diff --git a/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts b/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts index 9c81dd17d..f63462e04 100644 --- a/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts +++ b/libraries/search-javascript/lib/testcase/sampling/JavaScriptTestCaseSampler.ts @@ -36,6 +36,7 @@ import { ArrowFunctionStatement } from "../statements/complex/ArrowFunctionState import { ArrayStatement } from "../statements/complex/ArrayStatement"; import { ObjectStatement } from "../statements/complex/ObjectStatement"; import { IntegerStatement } from "../statements/primitive/IntegerStatement"; +import { ConstantPoolManager } from "@syntest/analysis-javascript"; /** * JavaScriptRandomSampler class @@ -43,6 +44,9 @@ import { IntegerStatement } from "../statements/primitive/IntegerStatement"; * @author Dimitri Stallenberg */ export abstract class JavaScriptTestCaseSampler extends EncodingSampler { + private _constantPoolManager: ConstantPoolManager; + private _constantPoolEnabled: boolean; + private _constantPoolProbability: number; private _typeInferenceMode: string; private _randomTypeProbability: number; private _incorporateExecutionInformation: boolean; @@ -55,6 +59,9 @@ export abstract class JavaScriptTestCaseSampler extends EncodingSampler { } 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, '\\"'); return [ { - decoded: `const ${this.varName} = "${this.value}";`, + decoded: `const ${this.varName} = "${value}";`, reference: this, }, ]; diff --git a/tools/javascript/lib/JavaScriptLauncher.ts b/tools/javascript/lib/JavaScriptLauncher.ts index ce7ddd3c6..cbc19fe85 100644 --- a/tools/javascript/lib/JavaScriptLauncher.ts +++ b/tools/javascript/lib/JavaScriptLauncher.ts @@ -33,6 +33,9 @@ import { DependencyFactory, TypeExtractor, isExported, + ConstantPoolManager, + ConstantVisitor, + getAllFiles, } from "@syntest/analysis-javascript"; import { ArgumentsObject, @@ -79,6 +82,7 @@ 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"; export type JavaScriptArguments = ArgumentsObject & TestCommandOptions; export class JavaScriptLauncher extends Launcher { @@ -115,6 +119,17 @@ export class JavaScriptLauncher extends Launcher { JavaScriptLauncher.LOGGER.info("Initialization started"); const start = Date.now(); + this.metricManager.recordProperty( + PropertyName.CONSTANT_POOL_ENABLED, + `${(this.arguments_).constantPool.toString()}` + ); + this.metricManager.recordProperty( + PropertyName.CONSTANT_POOL_PROBABILITY, + `${(( + this.arguments_ + )).constantPoolProbability.toString()}` + ); + this.storageManager.deleteTemporaryDirectories([ [this.arguments_.testDirectory], [this.arguments_.logDirectory], @@ -182,13 +197,15 @@ export class JavaScriptLauncher extends Launcher { async preprocess(): Promise { JavaScriptLauncher.LOGGER.info("Preprocessing started"); - const start = Date.now(); + const startPreProcessing = Date.now(); + + const startTargetSelection = Date.now(); const targetSelector = new TargetSelector(this.rootContext); this.targets = targetSelector.loadTargets( this.arguments_.include, this.arguments_.exclude ); - let timeInMs = (Date.now() - start) / 1000; + let timeInMs = (Date.now() - startTargetSelection) / 1000; this.metricManager.recordProperty( PropertyName.TARGET_LOAD_TIME, `${timeInMs}` @@ -281,10 +298,13 @@ export class JavaScriptLauncher extends Launcher { "Sample Output Values", `${this.arguments_.sampleFunctionOutputAsArgument}`, ], - ["Use Constant Pool Values", `${this.arguments_.constantPool}`], + [ + "Use Constant Pool Values", + `${(this.arguments_).constantPool}`, + ], [ "Use Constant Pool Probability", - `${this.arguments_.constantPoolProbability}`, + `${(this.arguments_).constantPoolProbability}`, ], ], footers: ["", ""], @@ -346,19 +366,18 @@ export class JavaScriptLauncher extends Launcher { this.rootContext.extractTypes(); JavaScriptLauncher.LOGGER.info("Resolving types"); this.rootContext.resolveTypes(); - JavaScriptLauncher.LOGGER.info("Preprocessing done"); - timeInMs = (Date.now() - startTypeResolving) / 1000; this.metricManager.recordProperty( PropertyName.TYPE_RESOLVE_TIME, `${timeInMs}` ); - timeInMs = (Date.now() - start) / 1000; + timeInMs = (Date.now() - startPreProcessing) / 1000; this.metricManager.recordProperty( PropertyName.PREPROCESS_TIME, `${timeInMs}` ); + JavaScriptLauncher.LOGGER.info("Preprocessing done"); } async process(): Promise { @@ -643,10 +662,39 @@ export class JavaScriptLauncher extends Launcher { this.arguments_.testDirectory ); - // TODO constant pool + JavaScriptLauncher.LOGGER.info("Extracting constants"); + const constantPoolManager = new ConstantPoolManager(); + const targetAbstractSyntaxTree = this.rootContext.getAbstractSyntaxTree( + target.path + ); + const constantVisitor = new ConstantVisitor( + target.path, + constantPoolManager.targetConstantPool + ); + traverse(targetAbstractSyntaxTree, constantVisitor); + + const files = getAllFiles(this.rootContext.rootPath, ".js").filter( + (x) => + !x.includes("/test/") && + !x.includes(".test.js") && + !x.includes("node_modules") + ); + + for (const file of files) { + const abstractSyntaxTree = this.rootContext.getAbstractSyntaxTree(file); + const constantVisitor = new ConstantVisitor( + file, + constantPoolManager.contextConstantPool + ); + traverse(abstractSyntaxTree, constantVisitor); + } + JavaScriptLauncher.LOGGER.info("Extracting constants done"); const sampler = new JavaScriptRandomSampler( currentSubject, + constantPoolManager, + (this.arguments_).constantPool, + (this.arguments_).constantPoolProbability, (this.arguments_).typeInferenceMode, (this.arguments_).randomTypeProbability, (this.arguments_).incorporateExecutionInformation, diff --git a/tools/javascript/lib/commands/test.ts b/tools/javascript/lib/commands/test.ts index c208827b4..bc8076c3b 100644 --- a/tools/javascript/lib/commands/test.ts +++ b/tools/javascript/lib/commands/test.ts @@ -33,6 +33,7 @@ export function getTestCommand( const options = new Map(); const commandGroup = "Type Inference Options:"; + const samplingGroup = "Sampling Options:"; options.set("incorporate-execution-information", { alias: [], @@ -62,6 +63,25 @@ export function getTestCommand( type: "number", }); + options.set("constant-pool", { + alias: [], + default: false, + description: "Enable constant pool.", + group: samplingGroup, + hidden: false, + type: "boolean", + }); + + options.set("constant-pool-probability", { + alias: [], + default: 0.5, + description: + "Probability to sample from the constant pool instead creating random values", + group: samplingGroup, + hidden: false, + type: "number", + }); + return new Command( moduleManager, tool, @@ -85,4 +105,6 @@ export type TestCommandOptions = { incorporateExecutionInformation: boolean; typeInferenceMode: string; randomTypeProbability: number; + constantPool: boolean; + constantPoolProbability: number; }; diff --git a/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts b/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts index caef0e680..fb5021e41 100644 --- a/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts +++ b/tools/javascript/lib/plugins/sampler/RandomSamplerPlugin.ts @@ -39,6 +39,9 @@ export class RandomSamplerPlugin extends SamplerPlugin { ): EncodingSampler { return new JavaScriptRandomSampler( options.subject as unknown as JavaScriptSubject, + undefined, + undefined, + undefined, ((this.args)).typeInferenceMode, ((this.args)).randomTypeProbability, ((