diff --git a/tests/ScanAndValidateParseTrees.spec.ts b/tests/ScanAndValidateParseTrees.spec.ts new file mode 100644 index 0000000..12b1a86 --- /dev/null +++ b/tests/ScanAndValidateParseTrees.spec.ts @@ -0,0 +1,7 @@ +import { File } from './support/infrastructure/File'; +import scanAndValidateParseTrees from './support/scanAndValidateParseTrees'; + +const INPUT_DIRECTORY: File = new File(__dirname, "../proleap-vb6/src/test/resources"); +const DIRECTORIES_EXCLUDED: File[] = [ new File(INPUT_DIRECTORY.getAbsolutePath(), "io/proleap/vb6/asg") ]; + +scanAndValidateParseTrees(INPUT_DIRECTORY, (directory) => Boolean(DIRECTORIES_EXCLUDED.find(d => d.equals(directory)))); diff --git a/tests/support/ThrowingErrorListener.ts b/tests/support/ThrowingErrorListener.ts new file mode 100644 index 0000000..5c721f2 --- /dev/null +++ b/tests/support/ThrowingErrorListener.ts @@ -0,0 +1,8 @@ +import { VbParserError } from './VbParserError'; +import { ANTLRErrorListener, Recognizer, RecognitionException } from "antlr4ts"; + +export class ThrowingErrorListener implements ANTLRErrorListener { + syntaxError(recognizer: Recognizer, offendingSymbol: T | undefined, line: number, charPositionInLine: number, msg: string, e: RecognitionException | undefined): void { + throw new VbParserError("syntax error in line " + line + ":" + charPositionInLine + " " + msg); + } +} diff --git a/tests/support/VbParseTestRunner.ts b/tests/support/VbParseTestRunner.ts new file mode 100644 index 0000000..a4d096a --- /dev/null +++ b/tests/support/VbParseTestRunner.ts @@ -0,0 +1,82 @@ +/** + * VB6 parse runner for Jest tests. + */ + +import { createLogger, ILogger } from './infrastructure/Logger'; +import { File } from './infrastructure/File'; +import { readFileSync } from 'fs'; +const readFileToString = (path: string): string => readFileSync(path, {encoding: 'utf8'}); +import { StartRuleContext, VisualBasic6Parser, VisualBasic6Lexer } from '../../index'; +import { ANTLRInputStream, CommonTokenStream } from "antlr4ts"; +import { cleanFileTree } from './cleanFileTree'; +import { IVbParserParams, VbParserParams } from './VbParserParams'; +import { ThrowingErrorListener } from './ThrowingErrorListener'; +import { isClazzModule, isStandardModule, isForm } from './vbFileTypes' + +export interface IVbParseTestRunner { + parseFile(inputFile: File): void; +} + +const LOG: ILogger = createLogger(__filename); + +const TREE_SUFFIX: string = ".tree"; + +export class VbParseTestRunner + implements IVbParseTestRunner { + + protected createDefaultParams(): IVbParserParams { + return new VbParserParams(); + } + + protected doCompareParseTree(treeFile: File, startRule: StartRuleContext, parser: VisualBasic6Parser): void { + const treeFileData: string = readFileToString(treeFile.getAbsolutePath()); + + if (treeFileData && treeFileData.length) { + LOG.info(`Comparing parse tree with file ${treeFile.getName()}.`); + + it(treeFile.getName(), () => { + const inputFileTree: string = startRule.toStringTree(parser); + const cleanedInputFileTree: string = cleanFileTree(inputFileTree); + const cleanedTreeFileData: string = cleanFileTree(treeFileData); + + expect(cleanedInputFileTree).toBe(cleanedTreeFileData); + }); + } else { + LOG.info(`Ignoring empty parse tree file ${treeFile.getName()}.`); + } + } + + protected doParse(inputFile: File, params: IVbParserParams): void { + LOG.info(`Parsing file ${inputFile.getName()}.`); + + const lexer: VisualBasic6Lexer = new VisualBasic6Lexer(new ANTLRInputStream(readFileToString(inputFile.getAbsolutePath()))); + + if (!params.getIgnoreSyntaxErrors()) { + lexer.removeErrorListeners(); + lexer.addErrorListener(new ThrowingErrorListener()); + } + + const tokens: CommonTokenStream = new CommonTokenStream(lexer); + const parser: VisualBasic6Parser = new VisualBasic6Parser(tokens); + + if (!params.getIgnoreSyntaxErrors()) { + parser.removeErrorListeners(); + parser.addErrorListener(new ThrowingErrorListener()); + } + + const startRule: StartRuleContext = parser.startRule(); + const treeFile: File = new File(inputFile.getAbsolutePath() + TREE_SUFFIX); + + if (treeFile.exists()) { + this.doCompareParseTree(treeFile, startRule, parser); + } + } + + parseFile(inputFile: File): void { + if (!isClazzModule(inputFile) && !isStandardModule(inputFile) && !isForm(inputFile)) { + LOG.info(`Ignoring file ${inputFile.getName()}.`); + } else { + this.doParse(inputFile, this.createDefaultParams()); + } + } +} diff --git a/tests/support/VbParserError.ts b/tests/support/VbParserError.ts new file mode 100644 index 0000000..ec9937e --- /dev/null +++ b/tests/support/VbParserError.ts @@ -0,0 +1,5 @@ +export class VbParserError extends Error { + constructor(message: string) { + super(message) + } +} diff --git a/tests/support/VbParserParams.ts b/tests/support/VbParserParams.ts new file mode 100644 index 0000000..7ff72ac --- /dev/null +++ b/tests/support/VbParserParams.ts @@ -0,0 +1,9 @@ +export interface IVbParserParams { + getIgnoreSyntaxErrors(): boolean; +} + +export class VbParserParams implements IVbParserParams { + getIgnoreSyntaxErrors(): boolean { + return false; + } +} diff --git a/tests/support/cleanFileTree.ts b/tests/support/cleanFileTree.ts new file mode 100644 index 0000000..9fba4b3 --- /dev/null +++ b/tests/support/cleanFileTree.ts @@ -0,0 +1,10 @@ +/** + * To be removed, as soon as the grammar does not require NEWLINEs and WS + * anymore + */ +export function cleanFileTree(input: string): string { + const inputNoEscapedNewline: string = input.replace(/\\r/g, "").replace(/\\n/g, "").replace(/\\t/g, "");; + const inputNoNewline: string = inputNoEscapedNewline.replace(/\r?\n|\r/g, ""); + const inputReducedWhitespace: string = inputNoNewline.replace(/[\s]+/g, " ").replace(/[\s]+\)/, ")"); + return inputReducedWhitespace; +} diff --git a/tests/support/infrastructure/File.ts b/tests/support/infrastructure/File.ts new file mode 100644 index 0000000..9eafc2e --- /dev/null +++ b/tests/support/infrastructure/File.ts @@ -0,0 +1,50 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +export class File { + private _path: string; + + constructor(value: string, ...additionalPathPieces: string[]) { + if (additionalPathPieces && additionalPathPieces.length) { + this._path = path.resolve(process.cwd(), value, ...additionalPathPieces); + } else { + this._path = path.resolve(process.cwd(), value); + } + } + + getName(): string { + return path.basename(this._path); + } + + getAbsolutePath(): string { + return path.resolve(this._path); + } + + exists(): boolean { + return fs.existsSync(this._path); + } + + removeExtension(): string { + return path.basename(this._path, path.extname(this._path)); + } + + equals(other: File): boolean { + return this.getAbsolutePath() === other.getAbsolutePath(); + } + + isDirectory(): boolean { + return fs.statSync(this._path).isDirectory(); + } + + isFile(): boolean { + return fs.statSync(this._path).isFile(); + } + + listFiles(): File[] { + return fs.readdirSync(this._path).map(e => new File(this._path, e)) + } + + getParent(): string { + return path.dirname(this._path); + } +} diff --git a/tests/support/infrastructure/Logger.ts b/tests/support/infrastructure/Logger.ts new file mode 100644 index 0000000..50a0dea --- /dev/null +++ b/tests/support/infrastructure/Logger.ts @@ -0,0 +1,27 @@ +export interface ILogger { + info(message: string): void; +} + +export class ConsoleLogger implements ILogger { + private _name: string; + + constructor(name: string) { + this._name = name; + } + + info(message: string): void { + console.log(`${this._name}: ${message}`); + } +} + +export class NullLogger implements ILogger { + constructor(name: string) { + } + + info(message: string): void { + } +} + +export function createLogger(name: string): ILogger { + return new NullLogger(name); +} diff --git a/tests/support/scanAndValidateParseTrees.ts b/tests/support/scanAndValidateParseTrees.ts new file mode 100644 index 0000000..7f45ab8 --- /dev/null +++ b/tests/support/scanAndValidateParseTrees.ts @@ -0,0 +1,35 @@ +import { File } from './infrastructure/File'; +import { isClazzModule, isStandardModule, isForm } from './vbFileTypes'; +import { IVbParseTestRunner, VbParseTestRunner } from './VbParseTestRunner'; + +function validateParseTrees(vb6InputFile: File): void { + const runner: IVbParseTestRunner = new VbParseTestRunner(); + runner.parseFile(vb6InputFile); +} + +export default function scanAndValidateParseTrees(inputDirectory: File, exclusionFilter: (file: File) => boolean) { + if (inputDirectory.isDirectory() && !exclusionFilter(inputDirectory)) { + describe(inputDirectory.getName(), () => { + // for each of the files in the directory + inputDirectory.listFiles().forEach(inputDirectoryFile => { + // if the file is a VB6 relevant file + if ( + inputDirectoryFile.isFile() + && (isClazzModule(inputDirectoryFile) + || isStandardModule(inputDirectoryFile) + || isForm(inputDirectoryFile))) { + validateParseTrees(inputDirectoryFile); + } + // else, if the file is a relevant directory + else if (inputDirectoryFile.isDirectory()) { + const subInputDirectory: File = inputDirectoryFile; + const subInputDirectoryName: string = subInputDirectory.getName(); + + if ("."!==subInputDirectoryName && ".."!==subInputDirectoryName) { + scanAndValidateParseTrees(subInputDirectory, exclusionFilter); + } + } + }); + }); + } +} diff --git a/tests/support/vbFileTypes.ts b/tests/support/vbFileTypes.ts new file mode 100644 index 0000000..141edf6 --- /dev/null +++ b/tests/support/vbFileTypes.ts @@ -0,0 +1,17 @@ +import { File } from './infrastructure/File'; +import { extname as getFileExtension } from 'path'; + +export function isClazzModule(inputFile: File): boolean { + const extension: string = getFileExtension(inputFile.getName()).toLowerCase(); + return ".cls" === extension; +} + +export function isStandardModule(inputFile: File): boolean { + const extension: string = getFileExtension(inputFile.getName()).toLowerCase(); + return ".bas" === extension; +} + +export function isForm(inputFile: File): boolean { + const extension: string = getFileExtension(inputFile.getName()).toLowerCase(); + return ".frm" === extension; +}