diff --git a/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts b/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts index d4c58c10..0183f3ec 100644 --- a/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts +++ b/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts @@ -13,7 +13,7 @@ function trimLeading(text: string) { return text.split('\n').map((line) => line.trimStart()).join('\n'); } -describe.only('MockUtil', () => { +describe('MockUtil', () => { let program: Program; let builder: ProgramBuilder; let plugin: RooibosPlugin; @@ -78,14 +78,14 @@ describe.only('MockUtil', () => { await builder.transpile(); let a = getContents('source/code.brs'); let b = trimLeading(`function sayHello(a1, a2) - if RBS_CC_1_getMocksByFunctionName()["sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["sayHello"].callback(a1,a2) + if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1,a2) return result end if print "hello" end function - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -109,14 +109,14 @@ describe.only('MockUtil', () => { await builder.transpile(); let a = getContents('source/code.brs'); let b = trimLeading(`function sayHello(a1, a2) - if RBS_CC_1_getMocksByFunctionName()["sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["sayHello"].callback(a1,a2) + if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1,a2) return result end if print "hello" end function - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -126,7 +126,7 @@ describe.only('MockUtil', () => { expect(a).to.equal(b); }); - it.only('weird raletracker task issue I saw', async () => { + it('weird raletracker task issue I saw', async () => { program.setFile('source/code.bs', ` Sub RedLines_SetRulerLines(rulerLines) For Each line In rulerLines.Items() @@ -142,15 +142,26 @@ describe.only('MockUtil', () => { expect(program.getDiagnostics()).to.be.empty; await builder.transpile(); let a = getContents('source/code.brs'); - let b = trimLeading(`function sayHello(a1, a2) - if RBS_CC_1_getMocksByFunctionName()["sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["sayHello"].callback(a1,a2) + let b = trimLeading(`Sub RedLines_SetRulerLines(rulerLines) + if RBS_SM_1_getMocksByFunctionName()["redlines_setrulerlines"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["redlines_setrulerlines"].callback(rulerLines) + return + end if + For Each line In rulerLines.Items() + RedLines_AddLine(line.key, line.value.position, line.value.coords, m.node, m.childMap) + End For + end Sub + + Sub RedLines_AddLine(id, position, coords, node, childMap) as Object + if RBS_SM_1_getMocksByFunctionName()["redlines_addline"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["redlines_addline"].callback(id,position,coords,node,childMap) return result end if - print "hello" - end function + line = CreateObject("roSGNode", "Rectangle") + line.setField("id", id) + end sub - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -172,14 +183,14 @@ describe.only('MockUtil', () => { await builder.transpile(); let a = getContents('source/code.brs'); let b = trimLeading(`sub sayHello(a1, a2) - if RBS_CC_1_getMocksByFunctionName()["sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["sayHello"].callback(a1,a2) + if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1,a2) return end if print "hello" end sub - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -203,14 +214,14 @@ describe.only('MockUtil', () => { await builder.transpile(); let a = getContents('source/code.brs'); let b = trimLeading(`function person_utils_sayHello(a1, a2) - if RBS_CC_1_getMocksByFunctionName()["person_utils_sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["person_utils_sayHello"].callback(a1,a2) + if RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"].callback(a1,a2) return result end if print "hello" end function - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -234,14 +245,14 @@ describe.only('MockUtil', () => { await builder.transpile(); let a = getContents('source/code.brs'); let b = trimLeading(`sub person_utils_sayHello(a1, a2) - if RBS_CC_1_getMocksByFunctionName()["person_utils_sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["person_utils_sayHello"].callback(a1,a2) + if RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"].callback(a1,a2) return end if print "hello" end sub - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -317,22 +328,22 @@ describe.only('MockUtil', () => { end function function beings_sayHello() - if RBS_CC_1_getMocksByFunctionName()["beings_sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["beings_sayHello"].callback() + if RBS_SM_1_getMocksByFunctionName()["beings_sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["beings_sayhello"].callback() return result end if print "hello2" end function function sayHello() - if RBS_CC_1_getMocksByFunctionName()["sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["sayHello"].callback() + if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback() return result end if print "hello3" end function - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if diff --git a/bsc-plugin/src/lib/rooibos/MockUtil.ts b/bsc-plugin/src/lib/rooibos/MockUtil.ts index 61e23b80..0c5b1a47 100644 --- a/bsc-plugin/src/lib/rooibos/MockUtil.ts +++ b/bsc-plugin/src/lib/rooibos/MockUtil.ts @@ -9,6 +9,11 @@ import { Range } from 'vscode-languageserver-types'; import type { FileFactory } from './FileFactory'; import undent from 'undent'; import type { RooibosSession } from './RooibosSession'; +import { BrsTranspileState } from 'brighterscript/dist/parser/BrsTranspileState'; +import { diagnosticErrorProcessingFile } from '../utils/Diagnostics'; +import type { TestCase } from './TestCase'; +import type { TestSuite } from './TestSuite'; +import { getAllDottedGetParts, getRootObjectFromDottedGet, getStringPathFromDottedGet, overrideAstTranspile } from './Utils'; export class MockUtil { @@ -22,7 +27,7 @@ export class MockUtil { session: RooibosSession; private brsFileAdditions = ` - function RBS_CC_#ID#_getMocksByFunctionName() + function RBS_SM_#ID#_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -34,21 +39,21 @@ export class MockUtil { private fileId: number; private filePathMap: any; private fileFactory: FileFactory; - private processedStatements: Set; + private processedStatements: Set; private astEditor: Editor; - public enableGlobalMethodMocks(file: BrsFile, astEditor: Editor) { + enableGlobalMethodMocks(file: BrsFile, astEditor: Editor) { if (this.config.isGlobalMethodMockingEnabled) { this._processFile(file, astEditor); } } - public _processFile(file: BrsFile, astEditor: Editor) { + _processFile(file: BrsFile, astEditor: Editor) { this.fileId++; - this.processedStatements = new Set(); + this.processedStatements = new Set(); this.astEditor = astEditor; - - for (let fs of file.parser.references.functionExpressions) { + // console.log('processing global methods on ', file.pkgPath); + for (let fs of file.parser.references.functionStatements) { this.enableMockOnFunction(fs); } @@ -58,44 +63,129 @@ export class MockUtil { } } - private enableMockOnFunction(functionExpression: brighterscript.FunctionExpression) { - if (isClassStatement(functionExpression.parent?.parent)) { - console.log('skipping class', functionExpression.parent?.parent.name.text) + private enableMockOnFunction(functionStatement: brighterscript.FunctionStatement) { + if (isClassStatement(functionStatement.parent?.parent)) { + // console.log('skipping class', functionStatement.parent?.parent?.name?.text); return; } - if (this.processedStatements.has(functionExpression)) { + if (this.processedStatements.has(functionStatement)) { + // console.log('skipping processed expression'); return; } - const methodName = functionExpression?.functionStatement?.getName(brighterscript.ParseMode.BrightScript) || ''; + const methodName = functionStatement?.getName(brighterscript.ParseMode.BrightScript).toLowerCase() || ''; // console.log('MN', methodName); if (this.config.isGlobalMethodMockingEfficientMode && !this.session.globalStubbedMethods.has(methodName)) { + // console.log('skipping method that is not stubbed', methodName); return; } + // console.log('processing stubbed method', methodName); //TODO check if the user has actually mocked or stubbed this function, otherwise leave it alone! - for (let param of functionExpression.parameters) { + for (let param of functionStatement.func.parameters) { param.asToken = null; } - const paramNames = functionExpression.parameters.map((param) => param.name.text).join(','); + const paramNames = functionStatement.func.parameters.map((param) => param.name.text).join(','); - const returnStatement = ((functionExpression.functionType?.kind === brighterscript.TokenKind.Sub && (functionExpression.returnTypeToken === undefined || functionExpression.returnTypeToken?.kind === brighterscript.TokenKind.Void)) || functionExpression.returnTypeToken?.kind === brighterscript.TokenKind.Void) ? 'return' : 'return result'; - this.astEditor.addToArray(functionExpression.body.statements, 0, new RawCodeStatement(undent` - if RBS_CC_${this.fileId}_getMocksByFunctionName()["${methodName}"] <> invalid - result = RBS_CC_${this.fileId}_getMocksByFunctionName()["${methodName}"].callback(${paramNames}) + const returnStatement = ((functionStatement.func.functionType?.kind === brighterscript.TokenKind.Sub && (functionStatement.func.returnTypeToken === undefined || functionStatement.func.returnTypeToken?.kind === brighterscript.TokenKind.Void)) || functionStatement.func.returnTypeToken?.kind === brighterscript.TokenKind.Void) ? 'return' : 'return result'; + this.astEditor.addToArray(functionStatement.func.body.statements, 0, new RawCodeStatement(undent` + if RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"] <> invalid + result = RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"].callback(${paramNames}) ${returnStatement} end if `)); - this.processedStatements.add(functionExpression); + this.processedStatements.add(functionStatement); } - public addBrsAPIText(file: BrsFile) { + addBrsAPIText(file: BrsFile) { //TODO should use ast editor! const func = new RawCodeStatement(this.brsFileAdditions.replace(/\#ID\#/g, this.fileId.toString().trim()), file, Range.create(Position.create(1, 1), Position.create(1, 1))); file.ast.statements.push(func); } + + gatherGlobalMethodMocks(testSuite: TestSuite) { + // console.log('gathering global method mocks for testSuite', testSuite.name); + for (let group of [...testSuite.testGroups.values()].filter((tg) => tg.isIncluded)) { + for (let testCase of [...group.testCases.values()].filter((tc) => tc.isIncluded)) { + this.gatherMockedGlobalMethods(testSuite, testCase); + } + } + + } + private gatherMockedGlobalMethods(testSuite: TestSuite, testCase: TestCase) { + try { + let func = testSuite.classStatement.methods.find((m) => m.name.text.toLowerCase() === testCase.funcName.toLowerCase()); + func.walk(brighterscript.createVisitor({ + ExpressionStatement: (expressionStatement, parent, owner) => { + let callExpression = expressionStatement.expression as brighterscript.CallExpression; + if (brighterscript.isCallExpression(callExpression) && brighterscript.isDottedGetExpression(callExpression.callee)) { + let dge = callExpression.callee; + let assertRegex = /(?:fail|assert(?:[a-z0-9]*)|expect(?:[a-z0-9]*)|stubCall)/i; + if (dge && assertRegex.test(dge.name.text)) { + if (dge.name.text === 'stubCall') { + this.processGlobalStubbedMethod(callExpression); + return expressionStatement; + + } else { + + if (dge.name.text === 'expectCalled' || dge.name.text === 'expectNotCalled') { + this.processGlobalStubbedMethod(callExpression); + } + } + } + } + } + }), { + walkMode: brighterscript.WalkMode.visitStatementsRecursive + }); + } catch (e) { + // console.log(e); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + diagnosticErrorProcessingFile(testSuite.file, e.message); + } + } + + private processGlobalStubbedMethod(callExpression: brighterscript.CallExpression) { + let isNotCalled = false; + let isStubCall = false; + const namespaceLookup = this.session.namespaceLookup; + if (brighterscript.isDottedGetExpression(callExpression.callee)) { + const nameText = callExpression.callee.name.text; + isNotCalled = nameText === 'expectNotCalled'; + isStubCall = nameText === 'stubCall'; + } + //modify args + let arg0 = callExpression.args[0]; + if (brighterscript.isCallExpression(arg0) && brighterscript.isDottedGetExpression(arg0.callee)) { + + //is it a namespace? + let dg = arg0.callee; + let nameParts = getAllDottedGetParts(dg); + let name = nameParts.pop(); + + // console.log('found expect with name', name); + if (name) { + //is a namespace? + if (nameParts[0] && namespaceLookup.has(nameParts[0].toLowerCase())) { + //then this must be a namespace method + let fullPathName = nameParts.join('.').toLowerCase(); + let ns = namespaceLookup.get(fullPathName); + if (!ns) { + //TODO this is an error condition! + } + nameParts.push(name); + let functionName = nameParts.join('_').toLowerCase(); + this.session.globalStubbedMethods.add(functionName); + } + } + } else if (brighterscript.isCallExpression(arg0) && brighterscript.isVariableExpression(arg0.callee)) { + let functionName = arg0.callee.getName(brighterscript.ParseMode.BrightScript).toLowerCase(); + this.session.globalStubbedMethods.add(functionName); + } + } + } diff --git a/bsc-plugin/src/lib/rooibos/RooibosSession.ts b/bsc-plugin/src/lib/rooibos/RooibosSession.ts index 40f423e3..757f5fd6 100644 --- a/bsc-plugin/src/lib/rooibos/RooibosSession.ts +++ b/bsc-plugin/src/lib/rooibos/RooibosSession.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import type { BrsFile, ClassStatement, FunctionStatement, NamespaceStatement, Program, ProgramBuilder } from 'brighterscript'; +import type { BrsFile, BscFile, ClassStatement, FunctionStatement, NamespaceStatement, Program, ProgramBuilder, Scope, Statement } from 'brighterscript'; import { isBrsFile, ParseMode, util } from 'brighterscript'; import type { AstEditor } from 'brighterscript/dist/astUtils/AstEditor'; import type { RooibosConfig } from './RooibosConfig'; @@ -12,11 +12,25 @@ import { diagnosticErrorNoMainFound as diagnosticWarnNoMainFound, diagnosticNoSt import undent from 'undent'; import { BrsTranspileState } from 'brighterscript/dist/parser/BrsTranspileState'; import * as fsExtra from 'fs-extra'; +import { MockUtil } from './MockUtil'; // eslint-disable-next-line const pkg = require('../../../package.json'); + +export interface NamespaceContainer { + file: BscFile; + fullName: string; + nameRange: Range; + lastPartName: string; + statements: Statement[]; + classStatements: Record; + functionStatements: Record; + namespaces: Record; +} + export class RooibosSession { + constructor(builder: ProgramBuilder, fileFactory: FileFactory) { this.fileFactory = fileFactory; this.config = builder.options ? (builder.options as any).rooibos as RooibosConfig || {} : {}; @@ -27,25 +41,42 @@ export class RooibosSession { private fileFactory: FileFactory; private _builder: ProgramBuilder; - public config: RooibosConfig; + config: RooibosConfig; + namespaceLookup: Map; private _suiteBuilder: TestSuiteBuilder; - public sessionInfo: SessionInfo; - public globalStubbedMethods = new Set(); - public reset() { + sessionInfo: SessionInfo; + globalStubbedMethods = new Set(); + + reset() { this.sessionInfo = new SessionInfo(this.config); } - public updateSessionStats() { + prepareForTranspile(editor: AstEditor, program: Program, mockUtil: MockUtil) { + this.addTestRunnerMetadata(editor); + this.addLaunchHookToExistingMain(editor); + if (this.config.isGlobalMethodMockingEnabled && this.config.isGlobalMethodMockingEfficientMode) { + console.log('Efficient global stubbing is enabled'); + this.namespaceLookup = this.getNamespaces(program); + for (let testSuite of this.sessionInfo.testSuitesToRun) { + mockUtil.gatherGlobalMethodMocks(testSuite); + } + + } else { + this.namespaceLookup = new Map(); + } + } + + updateSessionStats() { this.sessionInfo.updateInfo(); } - public processFile(file: BrsFile): boolean { + processFile(file: BrsFile): TestSuite[] { let testSuites = this._suiteBuilder.processFile(file); - return testSuites.length > 0; + return testSuites; } - public addLaunchHookToExistingMain(editor: AstEditor) { + addLaunchHookToExistingMain(editor: AstEditor) { let mainFunction: FunctionStatement; const files = this._builder.program.getScopeByName('source').getOwnFiles(); for (let file of files) { @@ -61,7 +92,7 @@ export class RooibosSession { editor.addToArray(mainFunction.func.body.statements, 0, new RawCodeStatement(`Rooibos_init("${this.config?.testSceneName ?? 'RooibosScene'}")`)); } } - public addLaunchHookFileIfNotPresent() { + addLaunchHookFileIfNotPresent() { let mainFunction: FunctionStatement; const files = this._builder.program.getScopeByName('source').getOwnFiles(); for (let file of files) { @@ -86,7 +117,7 @@ export class RooibosSession { } } - public addTestRunnerMetadata(editor: AstEditor) { + addTestRunnerMetadata(editor: AstEditor) { let runtimeConfig = this._builder.program.getFile('source/rooibos/RuntimeConfig.bs'); if (runtimeConfig) { let classStatement = (runtimeConfig.ast.statements[0] as NamespaceStatement).body.statements[0] as ClassStatement; @@ -98,7 +129,7 @@ export class RooibosSession { } } - public updateRunTimeConfigFunction(classStatement: ClassStatement, editor: AstEditor) { + updateRunTimeConfigFunction(classStatement: ClassStatement, editor: AstEditor) { let method = classStatement.methods.find((m) => m.name.text === 'getRuntimeConfig'); if (method) { editor.addToArray( @@ -122,7 +153,7 @@ export class RooibosSession { } } - public updateVersionTextFunction(classStatement: ClassStatement, editor: AstEditor) { + updateVersionTextFunction(classStatement: ClassStatement, editor: AstEditor) { let method = classStatement.methods.find((m) => m.name.text === 'getVersionText'); if (method) { editor.addToArray( @@ -133,7 +164,7 @@ export class RooibosSession { } } - public updateClassLookupFunction(classStatement: ClassStatement, editor: AstEditor) { + updateClassLookupFunction(classStatement: ClassStatement, editor: AstEditor) { let method = classStatement.methods.find((m) => m.name.text === 'getTestSuiteClassWithName'); if (method) { editor.arrayPush(method.func.body.statements, new RawCodeStatement(undent` @@ -146,7 +177,7 @@ export class RooibosSession { } } - public updateGetAllTestSuitesNames(classStatement: ClassStatement, editor: AstEditor) { + updateGetAllTestSuitesNames(classStatement: ClassStatement, editor: AstEditor) { let method = classStatement.methods.find((m) => m.name.text === 'getAllTestSuitesNames'); if (method) { editor.arrayPush(method.func.body.statements, new RawCodeStatement([ @@ -157,7 +188,7 @@ export class RooibosSession { } } - public createNodeFiles(program: Program) { + createNodeFiles(program: Program) { for (let suite of this.sessionInfo.testSuitesToRun.filter((s) => s.isNodeTest)) { this.createNodeFile(program, suite); @@ -189,7 +220,25 @@ export class RooibosSession { return this.fileFactory.createTestXML(suite.generatedNodeName, suite.nodeName); } - public createIgnoredTestsInfoFunction(cs: ClassStatement, editor: AstEditor) { + private getNamespaceLookup(scope: Scope): Map { + // eslint-disable-next-line @typescript-eslint/dot-notation + return scope['cache'].getOrAdd('namespaceLookup', () => scope.buildNamespaceLookup() as any); + } + + private getNamespaces(program: Program) { + let scopeNamespaces = new Map(); + for (const files of Object.values(program.files)) { + + for (let scope of program.getScopesForFile(files)) { + let scopeMap = this.getNamespaceLookup(scope); + scopeNamespaces = new Map([...Array.from(scopeMap.entries())]); + } + } + return scopeNamespaces; + } + + + private createIgnoredTestsInfoFunction(cs: ClassStatement, editor: AstEditor) { let method = cs.methods.find((m) => m.name.text === 'getIgnoredTestInfo'); if (method) { editor.arrayPush(method.func.body.statements, new RawCodeStatement([ diff --git a/bsc-plugin/src/lib/rooibos/TestGroup.ts b/bsc-plugin/src/lib/rooibos/TestGroup.ts index 9ebc7f7a..a638905b 100644 --- a/bsc-plugin/src/lib/rooibos/TestGroup.ts +++ b/bsc-plugin/src/lib/rooibos/TestGroup.ts @@ -9,9 +9,9 @@ import { RawCodeStatement } from './RawCodeStatement'; import type { TestCase } from './TestCase'; import type { TestSuite } from './TestSuite'; import { TestBlock } from './TestSuite'; -import { overrideAstTranspile, sanitizeBsJsonString } from './Utils'; +import { getAllDottedGetParts, getRootObjectFromDottedGet, getStringPathFromDottedGet, overrideAstTranspile, sanitizeBsJsonString } from './Utils'; import undent from 'undent'; -import type { NamespaceContainer } from '../../plugin'; +import type { NamespaceContainer } from './RooibosSession'; export class TestGroup extends TestBlock { @@ -56,43 +56,43 @@ export class TestGroup extends TestBlock { //wrap with if is not fail //add line number as last param const transpileState = new BrsTranspileState(this.file); - try { - let func = this.testSuite.classStatement.methods.find((m) => m.name.text.toLowerCase() === testCase.funcName.toLowerCase()); - func.walk(brighterscript.createVisitor({ - ExpressionStatement: (expressionStatement, parent, owner) => { - let callExpression = expressionStatement.expression as CallExpression; - if (brighterscript.isCallExpression(callExpression) && brighterscript.isDottedGetExpression(callExpression.callee)) { - let dge = callExpression.callee; - let isSub = isFunctionExpression(callExpression.parent.parent.parent) && callExpression.parent.parent.parent.functionType.kind === TokenKind.Sub; - let assertRegex = /(?:fail|assert(?:[a-z0-9]*)|expect(?:[a-z0-9]*)|stubCall)/i; - if (dge && assertRegex.test(dge.name.text)) { - if (dge.name.text === 'stubCall') { + // try { + let func = this.testSuite.classStatement.methods.find((m) => m.name.text.toLowerCase() === testCase.funcName.toLowerCase()); + func.walk(brighterscript.createVisitor({ + ExpressionStatement: (expressionStatement, parent, owner) => { + let callExpression = expressionStatement.expression as CallExpression; + if (brighterscript.isCallExpression(callExpression) && brighterscript.isDottedGetExpression(callExpression.callee)) { + let dge = callExpression.callee; + let isSub = isFunctionExpression(callExpression.parent.parent.parent) && callExpression.parent.parent.parent.functionType.kind === TokenKind.Sub; + let assertRegex = /(?:fail|assert(?:[a-z0-9]*)|expect(?:[a-z0-9]*)|stubCall)/i; + if (dge && assertRegex.test(dge.name.text)) { + if (dge.name.text === 'stubCall') { + this.modifyModernRooibosExpectCallExpression(callExpression, editor, namespaceLookup); + return expressionStatement; + + } else { + + if (dge.name.text === 'expectCalled' || dge.name.text === 'expectNotCalled') { this.modifyModernRooibosExpectCallExpression(callExpression, editor, namespaceLookup); - return expressionStatement; - - } else { - - if (dge.name.text === 'expectCalled' || dge.name.text === 'expectNotCalled') { - this.modifyModernRooibosExpectCallExpression(callExpression, editor, namespaceLookup); - } - //TODO change this to editor.setProperty(parentObj, parentKey, new SourceNode()) once bsc supports it - overrideAstTranspile(editor, expressionStatement, '\n' + undent` + } + //TODO change this to editor.setProperty(parentObj, parentKey, new SourceNode()) once bsc supports it + overrideAstTranspile(editor, expressionStatement, '\n' + undent` m.currentAssertLineNumber = ${callExpression.range.start.line} ${callExpression.transpile(transpileState).join('')} ${noEarlyExit ? '' : `if m.currentResult?.isFail = true then m.done() : return ${isSub ? '' : 'invalid'}`} ` + '\n'); - } } } } - }), { - walkMode: brighterscript.WalkMode.visitStatementsRecursive - }); - } catch (e) { - // console.log(e); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - diagnosticErrorProcessingFile(this.testSuite.file, e.message); - } + } + }), { + walkMode: brighterscript.WalkMode.visitStatementsRecursive + }); + // } catch (e) { + // // console.log(e); + // // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + // diagnosticErrorProcessingFile(this.testSuite.file, e.message); + // } } private modifyModernRooibosExpectCallExpression(callExpression: CallExpression, editor: AstEditor, namespaceLookup: Map) { @@ -110,7 +110,7 @@ export class TestGroup extends TestBlock { //is it a namespace? let dg = arg0.callee; - let nameParts = this.getAllDottedGetParts(dg); + let nameParts = getAllDottedGetParts(dg); let name = nameParts.pop(); if (name) { @@ -136,28 +136,28 @@ export class TestGroup extends TestBlock { this.testSuite.session.globalStubbedMethods.add(functionName); } else { let functionName = arg0.callee.name.text; - let fullPath = this.getStringPathFromDottedGet(arg0.callee.obj as DottedGetExpression); + let fullPath = getStringPathFromDottedGet(arg0.callee.obj as DottedGetExpression); editor.removeFromArray(callExpression.args, 0); if (!isNotCalled && !isStubCall) { const expectedArgs = new ArrayLiteralExpression(arg0.args, createToken(TokenKind.LeftSquareBracket), createToken(TokenKind.RightSquareBracket)); editor.addToArray(callExpression.args, 0, expectedArgs); } editor.addToArray(callExpression.args, 0, fullPath ?? createInvalidLiteral()); - editor.addToArray(callExpression.args, 0, this.getRootObjectFromDottedGet(arg0.callee)); + editor.addToArray(callExpression.args, 0, getRootObjectFromDottedGet(arg0.callee)); editor.addToArray(callExpression.args, 0, createStringLiteral(functionName)); editor.addToArray(callExpression.args, 0, arg0.callee.obj); } } } else if (brighterscript.isDottedGetExpression(arg0)) { let functionName = arg0.name.text; - let fullPath = this.getStringPathFromDottedGet(arg0.obj as DottedGetExpression); + let fullPath = getStringPathFromDottedGet(arg0.obj as DottedGetExpression); arg0 = callExpression.args[0] as DottedGetExpression; editor.removeFromArray(callExpression.args, 0); if (!isNotCalled && !isStubCall) { editor.addToArray(callExpression.args, 0, createInvalidLiteral()); } editor.addToArray(callExpression.args, 0, fullPath ?? createInvalidLiteral()); - editor.addToArray(callExpression.args, 0, this.getRootObjectFromDottedGet(arg0 as DottedGetExpression)); + editor.addToArray(callExpression.args, 0, getRootObjectFromDottedGet(arg0 as DottedGetExpression)); editor.addToArray(callExpression.args, 0, createStringLiteral(functionName)); editor.addToArray(callExpression.args, 0, (arg0 as DottedGetExpression).obj); } else if (brighterscript.isCallfuncExpression(arg0)) { @@ -171,9 +171,9 @@ export class TestGroup extends TestBlock { const expectedArgs = new ArrayLiteralExpression([createStringLiteral(functionName), ...arg0.args], createToken(TokenKind.LeftSquareBracket), createToken(TokenKind.RightSquareBracket)); editor.addToArray(callExpression.args, 0, expectedArgs); } - let fullPath = this.getStringPathFromDottedGet(arg0.callee as DottedGetExpression); + let fullPath = getStringPathFromDottedGet(arg0.callee as DottedGetExpression); editor.addToArray(callExpression.args, 0, fullPath ?? createInvalidLiteral()); - editor.addToArray(callExpression.args, 0, this.getRootObjectFromDottedGet(arg0.callee as DottedGetExpression)); + editor.addToArray(callExpression.args, 0, getRootObjectFromDottedGet(arg0.callee as DottedGetExpression)); editor.addToArray(callExpression.args, 0, createStringLiteral('callFunc')); editor.addToArray(callExpression.args, 0, arg0.callee); } else if (brighterscript.isCallExpression(arg0) && brighterscript.isVariableExpression(arg0.callee)) { @@ -208,68 +208,4 @@ export class TestGroup extends TestBlock { testCases: [${testCaseText.join(',\n')}] }`; } - - private getStringPathFromDottedGet(value: DottedGetExpression) { - let parts = [this.getPathValuePartAsString(value)]; - let root; - root = value.obj; - while (root) { - if (isCallExpression(root) || isCallfuncExpression(root)) { - return undefined; - } - parts.push(`${this.getPathValuePartAsString(root)}`); - root = root.obj; - } - let joinedParts = parts.reverse().join('.'); - return joinedParts === '' ? undefined : createStringLiteral(joinedParts); - } - - - private getPathValuePartAsString(expr: Expression) { - if (isCallExpression(expr) || isCallfuncExpression(expr)) { - return undefined; - } - if (isVariableExpression(expr)) { - return expr.name.text; - } - if (!expr) { - return undefined; - } - if (isDottedGetExpression(expr)) { - return expr.name.text; - } else if (isIndexedGetExpression(expr)) { - if (isLiteralExpression(expr.index)) { - return `${expr.index.token.text.replace(/^"/, '').replace(/"$/, '')}`; - } else if (isVariableExpression(expr.index)) { - return `${expr.index.name.text}`; - } - } - } - - private getRootObjectFromDottedGet(value: DottedGetExpression) { - let root; - if (isDottedGetExpression(value) || isIndexedGetExpression(value)) { - - root = value.obj; - while (root.obj) { - root = root.obj; - } - } else { - root = value; - } - - return root; - } - - getAllDottedGetParts(dg: DottedGetExpression) { - let parts = [dg?.name?.text]; - let nextPart = dg.obj; - while (isDottedGetExpression(nextPart) || isVariableExpression(nextPart)) { - parts.push(nextPart?.name?.text); - nextPart = isDottedGetExpression(nextPart) ? nextPart.obj : undefined; - } - return parts.reverse(); - } - - } diff --git a/bsc-plugin/src/lib/rooibos/TestSuite.ts b/bsc-plugin/src/lib/rooibos/TestSuite.ts index 741e5399..724fdd51 100644 --- a/bsc-plugin/src/lib/rooibos/TestSuite.ts +++ b/bsc-plugin/src/lib/rooibos/TestSuite.ts @@ -6,7 +6,7 @@ import type { RooibosAnnotation } from './Annotation'; import type { TestGroup } from './TestGroup'; import { addOverriddenMethod, sanitizeBsJsonString } from './Utils'; -import { RooibosSession } from './RooibosSession'; +import type { RooibosSession } from './RooibosSession'; /** * base of test suites and blocks.. diff --git a/bsc-plugin/src/lib/rooibos/Utils.ts b/bsc-plugin/src/lib/rooibos/Utils.ts index ecfbaa03..782607f6 100644 --- a/bsc-plugin/src/lib/rooibos/Utils.ts +++ b/bsc-plugin/src/lib/rooibos/Utils.ts @@ -52,3 +52,65 @@ export function addOverriddenMethod(file: BrsFile, annotation: AnnotationExpress export function sanitizeBsJsonString(text: string) { return `"${text ? text.replace(/"/g, '\'') : ''}"`; } + +export function getAllDottedGetParts(dg: brighterscript.DottedGetExpression) { + let parts = [dg?.name?.text]; + let nextPart = dg.obj; + while (brighterscript.isDottedGetExpression(nextPart) || brighterscript.isVariableExpression(nextPart)) { + parts.push(nextPart?.name?.text); + nextPart = brighterscript.isDottedGetExpression(nextPart) ? nextPart.obj : undefined; + } + return parts.reverse(); +} + + +export function getRootObjectFromDottedGet(value: brighterscript.DottedGetExpression) { + let root; + if (brighterscript.isDottedGetExpression(value) || brighterscript.isIndexedGetExpression(value)) { + + root = value.obj; + while (root.obj) { + root = root.obj; + } + } else { + root = value; + } + + return root; +} + +export function getStringPathFromDottedGet(value: brighterscript.DottedGetExpression) { + let parts = [this.getPathValuePartAsString(value)]; + let root; + root = value.obj; + while (root) { + if (brighterscript.isCallExpression(root) || brighterscript.isCallfuncExpression(root)) { + return undefined; + } + parts.push(`${this.getPathValuePartAsString(root)}`); + root = root.obj; + } + let joinedParts = parts.reverse().join('.'); + return joinedParts === '' ? undefined : brighterscript.createStringLiteral(joinedParts); +} + +export function getPathValuePartAsString(expr: Expression) { + if (brighterscript.isCallExpression(expr) || brighterscript.isCallfuncExpression(expr)) { + return undefined; + } + if (brighterscript.isVariableExpression(expr)) { + return expr.name.text; + } + if (!expr) { + return undefined; + } + if (brighterscript.isDottedGetExpression(expr)) { + return expr.name.text; + } else if (brighterscript.isIndexedGetExpression(expr)) { + if (brighterscript.isLiteralExpression(expr.index)) { + return `${expr.index.token.text.replace(/^"/, '').replace(/"$/, '')}`; + } else if (brighterscript.isVariableExpression(expr.index)) { + return `${expr.index.name.text}`; + } + } +} diff --git a/bsc-plugin/src/plugin.spec.ts b/bsc-plugin/src/plugin.spec.ts index e40d0b46..7abe626f 100644 --- a/bsc-plugin/src/plugin.spec.ts +++ b/bsc-plugin/src/plugin.spec.ts @@ -582,7 +582,7 @@ describe('RooibosPlugin', () => { expect(statements[0]).to.be.instanceof(PrintStatement); }); - describe('expectCalled transpilation', () => { + describe.skip('expectCalled transpilation', () => { it('correctly transpiles call funcs', async () => { program.setFile('source/test.spec.bs', ` @suite @@ -601,8 +601,9 @@ describe('RooibosPlugin', () => { expect(program.getDiagnostics()).to.be.empty; expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; await builder.transpile(); + const testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` m.currentAssertLineNumber = 6 m._expectCalled(m.thing, "callFunc", m, "m.thing", [ @@ -685,8 +686,9 @@ describe('RooibosPlugin', () => { await builder.transpile(); expect(program.getDiagnostics().filter((d) => d.code !== 'RBS2213')).to.be.empty; expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; + const testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` m.currentAssertLineNumber = 6 m._expectCalled(m.thing, "getFunction", m, "m.thing", []) @@ -781,8 +783,9 @@ describe('RooibosPlugin', () => { program.validate(); expect(program.getDiagnostics().filter((d) => d.code !== 'RBS2213')).to.be.empty; expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; + const testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` m.currentAssertLineNumber = 6 m._expectCalled(m.thing, "getFunction", m, "m.thing", []) @@ -832,8 +835,9 @@ describe('RooibosPlugin', () => { expect(program.getDiagnostics()).to.be.empty; // expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; await builder.transpile(); + const testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` b = { someValue: "value" @@ -865,8 +869,9 @@ describe('RooibosPlugin', () => { expect(program.getDiagnostics()).to.be.empty; expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; await builder.transpile(); + const testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` item = { id: "item" @@ -960,14 +965,14 @@ describe('RooibosPlugin', () => { let codeText = getContents('code.brs'); expect(codeText).to.equal(undent` function sayHello(firstName = "", lastName = "") - if RBS_CC_1_getMocksByFunctionName()["sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["sayHello"].callback(firstName,lastName) + if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(firstName,lastName) return result end if print firstName + " " + lastName end function - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -1037,14 +1042,14 @@ describe('RooibosPlugin', () => { let codeText = trimLeading(getContents('code.brs')); expect(codeText).to.equal(trimLeading(`function utils_sayHello(firstName = "", lastName = "") - if RBS_CC_1_getMocksByFunctionName()["utils_sayHello"] <> invalid - result = RBS_CC_1_getMocksByFunctionName()["utils_sayHello"].callback(firstName,lastName) + if RBS_SM_1_getMocksByFunctionName()["utils_sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["utils_sayhello"].callback(firstName,lastName) return result end if print firstName + " " + lastName end function - function RBS_CC_1_getMocksByFunctionName() + function RBS_SM_1_getMocksByFunctionName() if m._rMocksByFunctionName = invalid m._rMocksByFunctionName = {} end if @@ -1053,7 +1058,7 @@ describe('RooibosPlugin', () => { }); }); - describe('stubCall transpilation', () => { + describe.skip('stubCall transpilation', () => { it('correctly transpiles call funcs', async () => { program.setFile('source/test.spec.bs', ` @suite @@ -1195,7 +1200,7 @@ describe('RooibosPlugin', () => { }); }); - describe('expectNotCalled transpilation', () => { + describe.skip('expectNotCalled transpilation', () => { it('correctly transpiles call funcs', async () => { program.setFile('source/test.spec.bs', ` @suite @@ -1374,8 +1379,9 @@ describe('RooibosPlugin', () => { expect(program.getDiagnostics()).to.be.empty; expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; await builder.transpile(); + const testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` item = { id: "item" @@ -1520,7 +1526,7 @@ describe('RooibosPlugin', () => { }); describe('addTestRunnerMetadata', () => { - it('does not permanently modify the AST', async () => { + it.only('does not permanently modify the AST', async () => { program.setFile('source/test.spec.bs', ` @suite class ATest1 @@ -1528,6 +1534,7 @@ describe('RooibosPlugin', () => { @it("test1") function _() item = {id: "item"} + m.assertEqual(item, "wtf") m.expectNotCalled(item.getFunction()) m.expectNotCalled(item.getFunction()) end function @@ -1540,6 +1547,7 @@ describe('RooibosPlugin', () => { @it("test1") function _() item = {id: "item"} + m.assertEqual(item, "wtf") m.expectNotCalled(item.getFunction()) m.expectNotCalled(item.getFunction()) end function @@ -1563,9 +1571,9 @@ describe('RooibosPlugin', () => { expect(findMethod('getIgnoredTestInfo').func.body.statements).to.be.empty; await builder.transpile(); - let l = getTestFunctionContents(true); + let testContents = getTestFunctionContents(true); expect( - getTestFunctionContents(true) + testContents ).to.eql(undent` item = { id: "item" diff --git a/bsc-plugin/src/plugin.ts b/bsc-plugin/src/plugin.ts index b0ab49ba..3ab82410 100644 --- a/bsc-plugin/src/plugin.ts +++ b/bsc-plugin/src/plugin.ts @@ -24,17 +24,6 @@ import * as minimatch from 'minimatch'; import { MockUtil } from './lib/rooibos/MockUtil'; -export interface NamespaceContainer { - file: BscFile; - fullName: string; - nameRange: Range; - lastPartName: string; - statements: Statement[]; - classStatements: Record; - functionStatements: Record; - namespaces: Record; -} - export class RooibosPlugin implements CompilerPlugin { public name = 'rooibosPlugin'; @@ -81,7 +70,7 @@ export class RooibosPlugin implements CompilerPlugin { config.isGlobalMethodMockingEnabled = false; } if (config.isGlobalMethodMockingEfficientMode === undefined) { - config.isGlobalMethodMockingEfficientMode = false; + config.isGlobalMethodMockingEfficientMode = true; } if (config.keepAppOpen === undefined) { config.keepAppOpen = true; @@ -141,14 +130,12 @@ export class RooibosPlugin implements CompilerPlugin { // console.log('processing ', file.pkgPath); if (isBrsFile(file)) { - if (this.session.processFile(file)) { - } + this.session.processFile(file); } } beforeProgramTranspile(program: Program, entries: TranspileObj[], editor: AstEditor) { - this.session.addTestRunnerMetadata(editor); - this.session.addLaunchHookToExistingMain(editor); + this.session.prepareForTranspile(editor, program, this.mockUtil); } afterProgramTranspile(program: Program, entries: TranspileObj[], editor: AstEditor) { @@ -165,10 +152,9 @@ export class RooibosPlugin implements CompilerPlugin { } testSuite.addDataFunctions(event.editor as any); - const namespaceLookup = this.getNamespaces(testSuite.file); for (let group of [...testSuite.testGroups.values()].filter((tg) => tg.isIncluded)) { for (let testCase of [...group.testCases.values()].filter((tc) => tc.isIncluded)) { - group.modifyAssertions(testCase, noEarlyExit, event.editor as any, namespaceLookup); + group.modifyAssertions(testCase, noEarlyExit, event.editor as any, this.session.namespaceLookup); } } if (testSuite.isNodeTest) { @@ -234,6 +220,7 @@ export class RooibosPlugin implements CompilerPlugin { } else { for (let filter of this.config.globalMethodMockingExcludedFiles) { if (minimatch(file.pkgPath, filter)) { + // console.log('±±±skipping file', file.pkgPath); return false; } } @@ -241,19 +228,6 @@ export class RooibosPlugin implements CompilerPlugin { return true; } - public getNamespaceLookup(scope: Scope): Map { - // eslint-disable-next-line @typescript-eslint/dot-notation - return scope['cache'].getOrAdd('namespaceLookup', () => scope.buildNamespaceLookup() as any); - } - - private getNamespaces(file: BrsFile) { - let scopeNamespaces = new Map(); - for (let scope of file.program.getScopesForFile(file)) { - let scopeMap = this.getNamespaceLookup(scope); - scopeNamespaces = new Map([...Array.from(scopeMap.entries())]); - } - return scopeNamespaces; - } } export default () => {