From 10f6305a4606ae56ff16bb32b8c55fe7015f045d Mon Sep 17 00:00:00 2001 From: George Cook Date: Wed, 25 Oct 2023 22:34:06 +0200 Subject: [PATCH] WIP: adds support for expecting on global functions and namespace functions (#241) * adds support for expecting on global functions and namespace functions * fixes * wip * fixes error in utils * adds docs * reinstates erroneously disabled try catch --- bsc-plugin/src/lib/rooibos/MockUtil.spec.ts | 387 ++++++++++++++++++ bsc-plugin/src/lib/rooibos/MockUtil.ts | 191 +++++++++ bsc-plugin/src/lib/rooibos/RooibosConfig.ts | 3 + bsc-plugin/src/lib/rooibos/RooibosSession.ts | 81 +++- bsc-plugin/src/lib/rooibos/TestGroup.ts | 134 +++--- bsc-plugin/src/lib/rooibos/TestSuite.ts | 2 + .../src/lib/rooibos/TestSuiteBuilder.spec.ts | 84 ++-- .../src/lib/rooibos/TestSuiteBuilder.ts | 7 +- bsc-plugin/src/lib/rooibos/Utils.ts | 62 +++ bsc-plugin/src/plugin.spec.ts | 201 ++++++++- bsc-plugin/src/plugin.ts | 60 ++- docs/index.md | 92 +++-- framework/src/source/BaseTestSuite.bs | 330 ++++++++------- framework/src/source/CommonUtils.bs | 156 +++---- framework/src/source/ConsoleTestReporter.bs | 16 +- framework/src/source/Matchers.bs | 12 +- framework/src/source/RooibosScene.xml | 2 +- framework/src/source/TestRunner.bs | 6 +- tests/bsconfig.json | 3 +- tests/src/manifest | 5 +- tests/src/source/Common.spec.bs | 10 +- tests/src/source/Expect.spec.bs | 1 + tests/src/source/Globals.bs | 9 + tests/src/source/NewExpectSyntax.spec.bs | 72 +++- 24 files changed, 1499 insertions(+), 427 deletions(-) create mode 100644 bsc-plugin/src/lib/rooibos/MockUtil.spec.ts create mode 100644 bsc-plugin/src/lib/rooibos/MockUtil.ts create mode 100644 tests/src/source/Globals.bs diff --git a/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts b/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts new file mode 100644 index 00000000..0183f3ec --- /dev/null +++ b/bsc-plugin/src/lib/rooibos/MockUtil.spec.ts @@ -0,0 +1,387 @@ +/* eslint-disable @typescript-eslint/no-confusing-void-expression */ +import { Program, ProgramBuilder, util, standardizePath as s } from 'brighterscript'; +import { expect } from 'chai'; +import PluginInterface from 'brighterscript/dist/PluginInterface'; +import * as fsExtra from 'fs-extra'; +import { RooibosPlugin } from '../../plugin'; + +let tmpPath = s`${process.cwd()}/tmp`; +let _rootDir = s`${tmpPath}/rootDir`; +let _stagingFolderPath = s`${tmpPath}/staging`; + +function trimLeading(text: string) { + return text.split('\n').map((line) => line.trimStart()).join('\n'); +} + +describe('MockUtil', () => { + let program: Program; + let builder: ProgramBuilder; + let plugin: RooibosPlugin; + let options; + + function getContents(filename: string) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return trimLeading(fsExtra.readFileSync(s`${_stagingFolderPath}/${filename}`).toString()); + } + + describe('MockUtil', () => { + beforeEach(() => { + plugin = new RooibosPlugin(); + options = { + rootDir: _rootDir, + stagingFolderPath: _stagingFolderPath, + rooibos: { + isGlobalMethodMockingEnabled: true, + globalMethodMockingExcludedFiles: [ + '**/*.coverageExcluded.bs' + ], + isGlobalMethodMockingEfficientMode: false + }, + allowBrighterScriptInBrightScript: true + }; + fsExtra.ensureDirSync(_stagingFolderPath); + fsExtra.ensureDirSync(_rootDir); + fsExtra.ensureDirSync(tmpPath); + + builder = new ProgramBuilder(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + builder.options = util.normalizeAndResolveConfig(options); + builder.program = new Program(builder.options); + program = builder.program; + program.logger = builder.logger; + builder.plugins = new PluginInterface([plugin], { logger: builder.logger }); + program.plugins = new PluginInterface([plugin], { logger: builder.logger }); + program.createSourceScope(); //ensure source scope is created + plugin.beforeProgramCreate(builder); + + }); + afterEach(() => { + plugin.afterProgramCreate(program); + fsExtra.ensureDirSync(tmpPath); + fsExtra.emptyDirSync(tmpPath); + builder.dispose(); + program.dispose(); + }); + + describe('basic brs tests', () => { + + // This test fails unless `allowBrighterScriptInBrightScript` is set to true when setting up the program + // in `beforeEach`. This is because the compiler normally skips processing .brs files and copies them as-is. + it('adds util code to a brs file', async () => { + program.setFile('source/code.brs', ` + function sayHello(a1, a2) + print "hello" + end function + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`function sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + }); + describe('basic bs tests', () => { + + it('enables mocking on global functions', async () => { + program.setFile('source/code.bs', ` + function sayHello(a1, a2) + print "hello" + end function + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`function sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + it('weird raletracker task issue I saw', async () => { + program.setFile('source/code.bs', ` + Sub RedLines_SetRulerLines(rulerLines) + 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 + line = CreateObject("roSGNode", "Rectangle") + line.setField("id", id) + end sub + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + 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 + line = CreateObject("roSGNode", "Rectangle") + line.setField("id", id) + end sub + + function RBS_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + + it('enables mocking on global sub', async () => { + program.setFile('source/code.bs', ` + sub sayHello(a1, a2) + print "hello" + end sub + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`sub sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + + it('enables mocking on namespaced function', async () => { + program.setFile('source/code.bs', ` + namespace person.utils + function sayHello(a1, a2) + print "hello" + end function + end namespace + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`function person_utils_sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + + it('enables mocking on namespaced sub', async () => { + program.setFile('source/code.bs', ` + namespace person.utils + sub sayHello(a1, a2) + print "hello" + end sub + end namespace + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`sub person_utils_sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + + it('does not affect class methods', async () => { + program.setFile('source/code.bs', ` + class Person + sub sayHello(a1, a2) + print "hello" + end sub + end class + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`function __Person_builder() + instance = {} + instance.new = sub() + end sub + instance.sayHello = sub(a1, a2) + print "hello" + end sub + return instance + end function + function Person() + instance = __Person_builder() + instance.new() + return instance + end function`); + expect(a).to.equal(b); + + }); + it('will add stub code to namespace and global methods in a file with a class', async () => { + program.setFile('source/code.bs', ` + namespace beings + class Person + sub sayHello(a1, a2) + print "hello" + end sub + end class + function sayHello() + print "hello2" + end function + end namespace + function sayHello() + print "hello3" + end function + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + let a = getContents('source/code.brs'); + let b = trimLeading(`function __beings_Person_builder() + instance = {} + instance.new = sub() + end sub + instance.sayHello = sub(a1, a2) + print "hello" + end sub + return instance + end function + function beings_Person() + instance = __beings_Person_builder() + instance.new() + return instance + end function + + function beings_sayHello() + 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_SM_1_getMocksByFunctionName()["sayhello"] <> invalid + result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback() + return result + end if + print "hello3" + end function + + function RBS_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`); + expect(a).to.equal(b); + + }); + + }); + + it('excludes files from coverage', async () => { + const source = `sub foo() + x = function(y) + if (true) then + return 1 + end if + return 0 + end function + end sub`; + + program.setFile('source/code.coverageExcluded.bs', source); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + await builder.transpile(); + + let a = getContents('source/code.coverageExcluded.brs'); + let b = `sub foo() +x = function(y) +if (true) then +return 1 +end if +return 0 +end function +end sub`; + + expect(a).to.equal(b); + }); + }); +}); diff --git a/bsc-plugin/src/lib/rooibos/MockUtil.ts b/bsc-plugin/src/lib/rooibos/MockUtil.ts new file mode 100644 index 00000000..0c5b1a47 --- /dev/null +++ b/bsc-plugin/src/lib/rooibos/MockUtil.ts @@ -0,0 +1,191 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import type { BrsFile, Editor, ProgramBuilder } from 'brighterscript'; +import { Position, isClassStatement } from 'brighterscript'; +import * as brighterscript from 'brighterscript'; +import type { RooibosConfig } from './RooibosConfig'; +import { RawCodeStatement } from './RawCodeStatement'; +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 { + + constructor(builder: ProgramBuilder, fileFactory: FileFactory, session: RooibosSession) { + this.config = (builder.options as any).rooibos as RooibosConfig || {}; + this.filePathMap = {}; + this.fileId = 0; + this.session = session; + this.fileFactory = fileFactory; + } + session: RooibosSession; + + private brsFileAdditions = ` + function RBS_SM_#ID#_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function +`; + + private config: RooibosConfig; + private fileId: number; + private filePathMap: any; + private fileFactory: FileFactory; + private processedStatements: Set; + private astEditor: Editor; + + enableGlobalMethodMocks(file: BrsFile, astEditor: Editor) { + if (this.config.isGlobalMethodMockingEnabled) { + this._processFile(file, astEditor); + } + } + + _processFile(file: BrsFile, astEditor: Editor) { + this.fileId++; + this.processedStatements = new Set(); + this.astEditor = astEditor; + // console.log('processing global methods on ', file.pkgPath); + for (let fs of file.parser.references.functionStatements) { + this.enableMockOnFunction(fs); + } + + this.filePathMap[this.fileId] = file.pkgPath; + if (this.processedStatements.size > 0) { + this.addBrsAPIText(file); + } + } + + private enableMockOnFunction(functionStatement: brighterscript.FunctionStatement) { + if (isClassStatement(functionStatement.parent?.parent)) { + // console.log('skipping class', functionStatement.parent?.parent?.name?.text); + return; + } + if (this.processedStatements.has(functionStatement)) { + // console.log('skipping processed expression'); + return; + } + + 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 functionStatement.func.parameters) { + param.asToken = null; + } + const paramNames = functionStatement.func.parameters.map((param) => param.name.text).join(','); + + 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(functionStatement); + } + + 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/RooibosConfig.ts b/bsc-plugin/src/lib/rooibos/RooibosConfig.ts index 222cc38c..9e67cfae 100644 --- a/bsc-plugin/src/lib/rooibos/RooibosConfig.ts +++ b/bsc-plugin/src/lib/rooibos/RooibosConfig.ts @@ -7,8 +7,11 @@ export enum RooibosLogLevel { } export interface RooibosConfig { + isGlobalMethodMockingEfficientMode?: boolean; coverageExcludedFiles?: string[]; isRecordingCodeCoverage?: boolean; + isGlobalMethodMockingEnabled?: boolean; + globalMethodMockingExcludedFiles?: string[]; logLevel?: RooibosLogLevel; showOnlyFailures?: boolean; failFast?: boolean; diff --git a/bsc-plugin/src/lib/rooibos/RooibosSession.ts b/bsc-plugin/src/lib/rooibos/RooibosSession.ts index df415951..2af7551e 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 type { 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; + sessionInfo: SessionInfo; + globalStubbedMethods = new Set(); - public reset() { + 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 51a2e22c..91e34952 100644 --- a/bsc-plugin/src/lib/rooibos/TestGroup.ts +++ b/bsc-plugin/src/lib/rooibos/TestGroup.ts @@ -9,8 +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 './RooibosSession'; export class TestGroup extends TestBlock { @@ -49,7 +50,7 @@ export class TestGroup extends TestBlock { return [...this.testCases.values()]; } - public modifyAssertions(testCase: TestCase, noEarlyExit: boolean, editor: AstEditor) { + public modifyAssertions(testCase: TestCase, noEarlyExit: boolean, editor: AstEditor, namespaceLookup: Map) { //for each method //if assertion //wrap with if is not fail @@ -66,13 +67,13 @@ export class TestGroup extends TestBlock { 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); + this.modifyModernRooibosExpectCallExpression(callExpression, editor, namespaceLookup); return expressionStatement; } else { if (dge.name.text === 'expectCalled' || dge.name.text === 'expectNotCalled') { - this.modifyModernRooibosExpectCallExpression(callExpression, editor); + this.modifyModernRooibosExpectCallExpression(callExpression, editor, namespaceLookup); } //TODO change this to editor.setProperty(parentObj, parentKey, new SourceNode()) once bsc supports it overrideAstTranspile(editor, expressionStatement, '\n' + undent` @@ -94,7 +95,7 @@ export class TestGroup extends TestBlock { } } - private modifyModernRooibosExpectCallExpression(callExpression: CallExpression, editor: AstEditor) { + private modifyModernRooibosExpectCallExpression(callExpression: CallExpression, editor: AstEditor, namespaceLookup: Map) { let isNotCalled = false; let isStubCall = false; if (isDottedGetExpression(callExpression.callee)) { @@ -106,27 +107,57 @@ export class TestGroup extends TestBlock { //modify args let arg0 = callExpression.args[0]; if (brighterscript.isCallExpression(arg0) && isDottedGetExpression(arg0.callee)) { - let functionName = arg0.callee.name.text; - let fullPath = this.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); + + //is it a namespace? + let dg = arg0.callee; + let nameParts = getAllDottedGetParts(dg); + let name = nameParts.pop(); + + 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(); + 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, createInvalidLiteral()); + editor.addToArray(callExpression.args, 0, createInvalidLiteral()); + editor.addToArray(callExpression.args, 0, createStringLiteral(functionName)); + editor.addToArray(callExpression.args, 0, brighterscript.createVariableExpression(functionName)); + this.testSuite.session.globalStubbedMethods.add(functionName); + } else { + let functionName = arg0.callee.name.text; + 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, getRootObjectFromDottedGet(arg0.callee)); + editor.addToArray(callExpression.args, 0, createStringLiteral(functionName)); + editor.addToArray(callExpression.args, 0, arg0.callee.obj); + } } - editor.addToArray(callExpression.args, 0, fullPath ?? createInvalidLiteral()); - editor.addToArray(callExpression.args, 0, this.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)) { @@ -140,11 +171,23 @@ 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)) { + let functionName = arg0.callee.getName(brighterscript.ParseMode.BrightScript); + 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, createInvalidLiteral()); + editor.addToArray(callExpression.args, 0, createInvalidLiteral()); + editor.addToArray(callExpression.args, 0, createStringLiteral(functionName)); + editor.addToArray(callExpression.args, 0, brighterscript.createVariableExpression(functionName)); + this.testSuite.session.globalStubbedMethods.add(functionName); } } @@ -165,57 +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; - } - } diff --git a/bsc-plugin/src/lib/rooibos/TestSuite.ts b/bsc-plugin/src/lib/rooibos/TestSuite.ts index abd19f34..724fdd51 100644 --- a/bsc-plugin/src/lib/rooibos/TestSuite.ts +++ b/bsc-plugin/src/lib/rooibos/TestSuite.ts @@ -6,6 +6,7 @@ import type { RooibosAnnotation } from './Annotation'; import type { TestGroup } from './TestGroup'; import { addOverriddenMethod, sanitizeBsJsonString } from './Utils'; +import type { RooibosSession } from './RooibosSession'; /** * base of test suites and blocks.. @@ -80,6 +81,7 @@ export class TestSuite extends TestBlock { public generatedNodeName: string; public hasSoloGroups = false; public isNodeTest = false; + public session: RooibosSession; public addGroup(group: TestGroup) { this.testGroups.set(group.name, group); diff --git a/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.spec.ts b/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.spec.ts index a6fc7d5e..75c8e18f 100644 --- a/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.spec.ts +++ b/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.spec.ts @@ -78,7 +78,7 @@ describe('TestSuiteBuilder tests ', () => { it('duplicate suite name - different files', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -90,7 +90,7 @@ describe('TestSuiteBuilder tests ', () => { assertSuite(ts, 1); let testSuite2 = createTestSuite('test2.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -106,7 +106,7 @@ describe('TestSuiteBuilder tests ', () => { it('duplicate suite name - same file', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -114,7 +114,7 @@ describe('TestSuiteBuilder tests ', () => { end function end class @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -129,7 +129,7 @@ describe('TestSuiteBuilder tests ', () => { it('duplicate group', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -150,7 +150,7 @@ describe('TestSuiteBuilder tests ', () => { it('duplicate test', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -173,7 +173,7 @@ describe('TestSuiteBuilder tests ', () => { let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @describe("group1") @@ -194,7 +194,7 @@ end namespace let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @describe("group1") @@ -218,7 +218,7 @@ end namespace let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @describe("group1") @@ -248,7 +248,7 @@ end namespace let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @@ -274,7 +274,7 @@ end namespace let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @@ -294,7 +294,7 @@ end namespace let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @@ -316,7 +316,7 @@ end namespace let testSuite = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @@ -344,7 +344,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @only @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -370,7 +370,7 @@ end namespace it('only group', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @only @describe("group1") @@ -397,7 +397,7 @@ end namespace it('only test', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @only @@ -424,7 +424,7 @@ end namespace it('two tests', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @only @@ -451,7 +451,7 @@ end namespace it('two tests and group', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @only @describe("group1") @@ -480,7 +480,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @only @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @only @describe("group1") @@ -509,7 +509,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @only @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @only @describe("group1") @@ -546,7 +546,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @only @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @only @describe("group1") @@ -582,7 +582,7 @@ end namespace it('only on param block', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @Only @@ -615,7 +615,7 @@ end namespace it('onlyparams', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -647,7 +647,7 @@ end namespace it('onlyparams 2', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -683,7 +683,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @ignore @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -708,7 +708,7 @@ end namespace it('ignore group', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @ignore @describe("group1") @@ -733,7 +733,7 @@ end namespace it('ignore test', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @ignore @@ -760,7 +760,7 @@ end namespace it('two tests', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @ignore @@ -787,7 +787,7 @@ end namespace it('two tests and group', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @ignore @describe("group1") @@ -816,7 +816,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @ignore @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @ignore @describe("group1") @@ -846,7 +846,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @ignore @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @ignore @describe("group1") @@ -885,7 +885,7 @@ end namespace let ts = createTestSuite('test1.bs', `namespace Tests @ignore @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @ignore @describe("group1") @@ -923,7 +923,7 @@ end namespace it('ignore on param block', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @ignore @@ -956,7 +956,7 @@ end namespace it('ignoreParams', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -988,7 +988,7 @@ end namespace it('ignoreParams 2', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1023,7 +1023,7 @@ end namespace it('simple params', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1042,7 +1042,7 @@ end namespace it('2 params', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1062,7 +1062,7 @@ end namespace it('2 with url and chars', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1081,7 +1081,7 @@ end namespace it('param mismatch -no params', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1095,7 +1095,7 @@ end namespace it('param mismatch -one', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1113,7 +1113,7 @@ end namespace it('param mismatch -all', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") @@ -1131,7 +1131,7 @@ end namespace it('cannot parse', () => { let ts = createTestSuite('test1.bs', `namespace Tests @suite("Rooibos assertion tests") - class AssertionTests extends Rooibos.BaseTestSuite + class AssertionTests extends rooibos.BaseTestSuite @describe("group1") @it("one") diff --git a/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.ts b/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.ts index 36cbeeff..e7dfac3b 100644 --- a/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.ts +++ b/bsc-plugin/src/lib/rooibos/TestSuiteBuilder.ts @@ -1,7 +1,9 @@ -import type { BrsFile, +import type { + BrsFile, ClassMethodStatement, - ClassStatement } from 'brighterscript'; + ClassStatement +} from 'brighterscript'; import { isClassMethodStatement } from 'brighterscript'; @@ -97,6 +99,7 @@ export class TestSuiteBuilder { public processClass(annotation: RooibosAnnotation, classStatement: ClassStatement): TestSuite { this.testSuite = new TestSuite(annotation, classStatement); + this.testSuite.session = this.session; this.currentGroup = null; this.annotation = null; for (let s of classStatement.body) { diff --git a/bsc-plugin/src/lib/rooibos/Utils.ts b/bsc-plugin/src/lib/rooibos/Utils.ts index ecfbaa03..277ee654 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 = [getPathValuePartAsString(value)]; + let root; + root = value.obj; + while (root) { + if (brighterscript.isCallExpression(root) || brighterscript.isCallfuncExpression(root)) { + return undefined; + } + parts.push(`${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 881352aa..c1150218 100644 --- a/bsc-plugin/src/plugin.spec.ts +++ b/bsc-plugin/src/plugin.spec.ts @@ -21,7 +21,10 @@ describe('RooibosPlugin', () => { options = { rootDir: _rootDir, stagingFolderPath: _stagingFolderPath, - stagingDir: _stagingFolderPath + stagingDir: _stagingFolderPath, + rooibos: { + isGlobalMethodMockingEnabled: true + } }; fsExtra.ensureDirSync(_stagingFolderPath); fsExtra.ensureDirSync(_rootDir); @@ -579,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 @@ -598,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", [ @@ -682,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", []) @@ -778,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", []) @@ -829,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" @@ -862,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" @@ -895,9 +903,162 @@ describe('RooibosPlugin', () => { if m.currentResult?.isFail = true then m.done() : return invalid `); }); + it('correctly transpiles global function calls', async () => { + + program.setFile('source/test.spec.bs', ` + @suite + class ATest + @describe("groupA") + @it("test1") + function _() + item = {id: "item"} + m.expectCalled(sayHello("arg1", "arg2"), "return") + m.expectCalled(sayHello()) + m.expectCalled(sayHello(), "return") + m.expectCalled(sayHello("arg1", "arg2")) + end function + end class + `); + program.setFile('source/code.bs', ` + function sayHello(firstName = "" as string, lastName = "" as string) + print firstName + " " + lastName + end function + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; + await builder.transpile(); + const testText = getTestFunctionContents(true); + expect( + testText + ).to.eql(undent` + item = { + id: "item" + } + + m.currentAssertLineNumber = 7 + m._expectCalled(sayHello, "sayHello", invalid, invalid, [ + "arg1" + "arg2" + ], "return") + if m.currentResult?.isFail = true then m.done() : return invalid + + + m.currentAssertLineNumber = 8 + m._expectCalled(sayHello, "sayHello", invalid, invalid, []) + if m.currentResult?.isFail = true then m.done() : return invalid + + + m.currentAssertLineNumber = 9 + m._expectCalled(sayHello, "sayHello", invalid, invalid, [], "return") + if m.currentResult?.isFail = true then m.done() : return invalid + + + m.currentAssertLineNumber = 10 + m._expectCalled(sayHello, "sayHello", invalid, invalid, [ + "arg1" + "arg2" + ]) + if m.currentResult?.isFail = true then m.done() : return invalid +`); + + let codeText = getContents('code.brs'); + expect(codeText).to.equal(undent` + function sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function`); + }); + it('correctly transpiles namespaced function calls', async () => { + plugin.config.isGlobalMethodMockingEnabled = true; + program.setFile('source/test.spec.bs', ` + @suite + class ATest + @describe("groupA") + @it("test1") + function _() + item = {id: "item"} + m.expectCalled(utils.sayHello("arg1", "arg2"), "return") + m.expectCalled(utils.sayHello()) + m.expectCalled(utils.sayHello(), "return") + m.expectCalled(utils.sayHello("arg1", "arg2")) + end function + end class + `); + program.setFile('source/code.bs', ` + namespace utils + function sayHello(firstName = "" as string, lastName = "" as string) + print firstName + " " + lastName + end function + end namespace + `); + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + expect(plugin.session.sessionInfo.testSuitesToRun).to.not.be.empty; + await builder.transpile(); + const testText = getTestFunctionContents(true); + expect( + testText + ).to.eql(undent` + item = { + id: "item" + } + + m.currentAssertLineNumber = 7 + m._expectCalled(utils_sayhello, "utils_sayhello", invalid, invalid, [ + "arg1" + "arg2" + ], "return") + if m.currentResult?.isFail = true then m.done() : return invalid + + + m.currentAssertLineNumber = 8 + m._expectCalled(utils_sayhello, "utils_sayhello", invalid, invalid, []) + if m.currentResult?.isFail = true then m.done() : return invalid + + + m.currentAssertLineNumber = 9 + m._expectCalled(utils_sayhello, "utils_sayhello", invalid, invalid, [], "return") + if m.currentResult?.isFail = true then m.done() : return invalid + + + m.currentAssertLineNumber = 10 + m._expectCalled(utils_sayhello, "utils_sayhello", invalid, invalid, [ + "arg1" + "arg2" + ]) + if m.currentResult?.isFail = true then m.done() : return invalid +`); + + let codeText = trimLeading(getContents('code.brs')); + expect(codeText).to.equal(trimLeading(`function utils_sayHello(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_SM_1_getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function`)); + }); }); - describe('stubCall transpilation', () => { + describe.skip('stubCall transpilation', () => { it('correctly transpiles call funcs', async () => { program.setFile('source/test.spec.bs', ` @suite @@ -1039,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 @@ -1218,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" @@ -1407,9 +1569,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" @@ -1438,15 +1600,15 @@ describe('RooibosPlugin', () => { end function instance.getRuntimeConfig = function() return { - "failFast": false - "sendHomeOnFinish": false + "failFast": true + "sendHomeOnFinish": true "logLevel": 0 - "showOnlyFailures": false - "printTestTimes": false + "showOnlyFailures": true + "printTestTimes": true "lineWidth": 60 "printLcov": false "port": "invalid" - "catchCrashes": false + "catchCrashes": true "keepAppOpen": true } end function @@ -1578,3 +1740,8 @@ function getTestSubContents(trimEveryLine = false) { } return result; } + +function trimLeading(text: string) { + return text.split('\n').map((line) => line.trimStart()).join('\n'); +} + diff --git a/bsc-plugin/src/plugin.ts b/bsc-plugin/src/plugin.ts index 84c38d6e..ffbf4750 100644 --- a/bsc-plugin/src/plugin.ts +++ b/bsc-plugin/src/plugin.ts @@ -6,7 +6,12 @@ import type { TranspileObj, AstEditor, BeforeFileTranspileEvent, - PluginHandler + PluginHandler, + ClassStatement, + FunctionStatement, + Statement, + Scope, + BrsFile } from 'brighterscript'; import { isBrsFile @@ -16,12 +21,14 @@ import { CodeCoverageProcessor } from './lib/rooibos/CodeCoverageProcessor'; import { FileFactory } from './lib/rooibos/FileFactory'; import type { RooibosConfig } from './lib/rooibos/RooibosConfig'; import * as minimatch from 'minimatch'; +import { MockUtil } from './lib/rooibos/MockUtil'; export class RooibosPlugin implements CompilerPlugin { public name = 'rooibosPlugin'; public session: RooibosSession; public codeCoverageProcessor: CodeCoverageProcessor; + public mockUtil: MockUtil; public fileFactory: FileFactory; public _builder: ProgramBuilder; public config: RooibosConfig; @@ -35,6 +42,7 @@ export class RooibosPlugin implements CompilerPlugin { if (!this.session) { this.session = new RooibosSession(builder, this.fileFactory); this.codeCoverageProcessor = new CodeCoverageProcessor(builder, this.fileFactory); + this.mockUtil = new MockUtil(builder, this.fileFactory, this.session); } } private getConfig(options: any) { @@ -57,6 +65,12 @@ export class RooibosPlugin implements CompilerPlugin { if (config.isRecordingCodeCoverage === undefined) { config.isRecordingCodeCoverage = false; } + if (config.isGlobalMethodMockingEnabled === undefined) { + config.isGlobalMethodMockingEnabled = false; + } + if (config.isGlobalMethodMockingEfficientMode === undefined) { + config.isGlobalMethodMockingEfficientMode = true; + } if (config.keepAppOpen === undefined) { config.keepAppOpen = true; } @@ -85,6 +99,16 @@ export class RooibosPlugin implements CompilerPlugin { config.coverageExcludedFiles.push(...defaultCoverageExcluded); } + const defaultGlobalMethodMockingExcluded = [ + '**/*.spec.bs', + '**/roku_modules/**/*', + '**/source/main.bs', + '**/source/rooibos/**/*' + ]; + if (config.globalMethodMockingExcludedFiles === undefined) { + config.globalMethodMockingExcludedFiles = defaultGlobalMethodMockingExcluded; + } + return config; } @@ -105,14 +129,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) { @@ -131,7 +153,7 @@ export class RooibosPlugin implements CompilerPlugin { testSuite.addDataFunctions(event.editor as any); 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); + group.modifyAssertions(testCase, noEarlyExit, event.editor as any, this.session.namespaceLookup); } } if (testSuite.isNodeTest) { @@ -139,8 +161,13 @@ export class RooibosPlugin implements CompilerPlugin { } } - if (isBrsFile(event.file) && this.shouldAddCodeCoverageToFile(event.file)) { - this.codeCoverageProcessor.addCodeCoverage(event.file, event.editor); + if (isBrsFile(event.file)) { + if (this.shouldAddCodeCoverageToFile(event.file)) { + this.codeCoverageProcessor.addCodeCoverage(event.file, event.editor); + } + if (this.shouldEnableGlobalMocksOnFile(event.file)) { + this.mockUtil.enableGlobalMethodMocks(event.file, event.editor); + } } } @@ -183,6 +210,23 @@ export class RooibosPlugin implements CompilerPlugin { } return true; } + + shouldEnableGlobalMocksOnFile(file: BscFile) { + if (!isBrsFile(file) || !this.config.isGlobalMethodMockingEnabled) { + return false; + } else if (!this.config.globalMethodMockingExcludedFiles) { + return true; + } else { + for (let filter of this.config.globalMethodMockingExcludedFiles) { + if (minimatch(file.pkgPath, filter)) { + // console.log('±±±skipping file', file.pkgPath); + return false; + } + } + } + return true; + } + } export default () => { diff --git a/docs/index.md b/docs/index.md index 48507e23..4aa2b902 100644 --- a/docs/index.md +++ b/docs/index.md @@ -156,18 +156,21 @@ The following options are supported: Here is the information converted into a Markdown table: -| Property | Type | Description | -|-------------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| logLevel? | RooibosLogLevel | 0-4 (error, warn, info, debug) | -| showOnlyFailures? | boolean | If true, then only failed tests are shown; but everything is shown if no failures occurred | -| printTestTimes? | boolean | If true, then the time each test took is output | -| lineWidth? | number | Width of test output lines in columns | -| catchCrashes? | boolean | If true, then any crashes will report CRASH statement, and note halt test execution - very useful for running a whole suite | -| sendHomeOnFinish? | boolean | If true, then the app will exit upon finish. The default is true. Useful to set to false for local test suites | -| keepAppOpen? | boolean | When true, the app will remain open upon test completion. The default is true. Set false to return execution to Main | -| testsFilePattern? | string | The pattern to use to find tests. This is a glob. The default is "**/*.spec.bs" | -| tags? | string[] | The tags listed here control what is run. You can use !tagname to indicate any tests/suites that are skipped. All other tags are ANDed. This is useful for running specific subsets of your suite. | -| testSceneName | string | Test scene to use for the test run. Provide a different name here if you need custom setup in your scene. You should extend or duplicate the RooibosScene component (found in RooibosScene.xml) | +| Property | Type | Description | +|------------------------------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| logLevel? | RooibosLogLevel | 0-4 (error, warn, info, debug) | +| showOnlyFailures? | boolean | If true, then only failed tests are shown; but everything is shown if no failures occurred | +| printTestTimes? | boolean | If true, then the time each test took is output | +| lineWidth? | number | Width of test output lines in columns | +| catchCrashes? | boolean | If true, then any crashes will report CRASH statement, and note halt test execution - very useful for running a whole suite | +| sendHomeOnFinish? | boolean | If true, then the app will exit upon finish. The default is true. Useful to set to false for local test suites | +| keepAppOpen? | boolean | When true, the app will remain open upon test completion. The default is true. Set false to return execution to Main | +| testsFilePattern? | string | The pattern to use to find tests. This is a glob. The default is "**/*.spec.bs" | +| tags? | string[] | The tags listed here control what is run. You can use !tagname to indicate any tests/suites that are skipped. All other tags are ANDed. This is useful for running specific subsets of your suite. | +| testSceneName | string | Test scene to use for the test run. Provide a different name here if you need custom setup in your scene. You should extend or duplicate the RooibosScene component (found in RooibosScene.xml) | +| isGlobalMethodMockingEnabled | boolean | Default is false. Enables mocking and stubbing support for global and namespace functions | +| isGlobalMethodMockingEfficientMode | boolean | default to true, when set causes rooibos to modify only those functions that were mocked or stubbed | +| globalMethodMockingExcludedFiles | string[] | Files that rooibos will not modify when adding global function or namespace function mocking support | ## Creating test suites @@ -183,7 +186,7 @@ Rooibos has a hierarchy of tests as follows: Test suites are defined by: - declaring a class _inside_ a namespace - - which extends `Rooibos.BaseTestSuite` + - which extends `rooibos.BaseTestSuite` - and has a `@suite` annotation No special file naming is required. I recommend you call your files `thing.spec.bs` @@ -192,7 +195,7 @@ Please note that rooibos tests are _brighterscript_ only. You can test regular b ### Some advice -I find it really handy to have my own BaseTestSuite, that extends `Rooibos.BaseTestSuite` and I use that as the base of all my tests. In this way I can easily use common utilities and use common beforeEach/setup for setting things up. +I find it really handy to have my own BaseTestSuite, that extends `rooibos.BaseTestSuite` and I use that as the base of all my tests. In this way I can easily use common utilities and use common beforeEach/setup for setting things up. ### Simple example The following is a minimum working example of a Rooibos test suite, named `Simple.brs` @@ -202,7 +205,7 @@ The following is a minimum working example of a Rooibos test suite, named `Simpl namespace tests @suite("basic tests") - class BasicTests extends Rooibos.BaseTestSuite + class BasicTests extends rooibos.BaseTestSuite '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @describe("tests the node context is available for a Node scope function") @@ -227,7 +230,7 @@ Note, you can also use sub, if you wish namespace tests @suite("basic tests") - class BasicTests extends Rooibos.BaseTestSuite + class BasicTests extends rooibos.BaseTestSuite '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @describe("tests the node context is available for a Node scope function") @@ -429,7 +432,7 @@ In addition, we can also use beforeEach and afterEach to run before **each and e ``` namespace Tests - class SampleTest extends Rooibos.BaseTestSuite + class SampleTest extends rooibos.BaseTestSuite override function setup() m.values = [{index:1,name:"one"},{index:4, name:"four"},{index:12, name:"twelve"}] @@ -714,6 +717,52 @@ For the regular class method variation, you can simply pass a pointer to the fun - expectOnce - Creates a mock, which we expect to be called once _or can created individual overloaded calls to the same method_ - expectNone - Creates a mock, which we _never_ expect to be invoked +## Mocking global and namespaced functions + +Mocking and stubbing of global functions works the same as for class methods. Please note, the functions must be in scope - you cannot mock a global function call inside of a node. So if your call crosses an observer, or callfunc bridge, it will not work. This is by design, and there are no plans to provide intra-node global or namespace mocking, at this point. + +To enable this feature, set the `isGlobalMethodMockingEnabled` rooibos config parameter to true. **DO NOT** enable this on your ide's bsconfig.json, as it will negatively impact performance and may cause other issues. This setting is not compatible with watch mode, at this moment. + +### Configuration +In addition to the `isGlobalMethodMockingEnabled`, you use the following config flags: + + - `isGlobalMethodMockingEfficientMode` - default to true, when set causes rooibos to modify only those functions that were mocked or stubbed + - `globalMethodMockingExcludedFiles` - files that rooibos will not modify, when adding global mocking or stubbing support. + +### Known limitations: + +#### You must include default values in your expect calls, even if your invoking code does not use them + - if you mock or stub a global or namespaced method, you will have to expect default parameters in your expectCalled invocation, as rooibos will receive the default values. + +e.g. +``` +namespace utils + function getModel(name as string, asJson = false as boolean) + end function +end namespace + +'in the code +function getBeatlesModel() + utils.getModel("Ringo") +end function + +'in the unit test +@describe("getBeatlesModel") +@it("gets ringo") +function _() + 'This will fail, because rooibos will receive the asJson param with the default value + m.expectCalled(utils.getModel("ringo")) + + 'therefore we need to do this + m.expectCalled(utils.getModel("ringo", false)) +end function +``` + +Note - this will be addressed shortly; pr's are welcome in the interim. + +#### There might be some bugs, or unknown issues +Mocking, of global and namespaced functions is supported; but is a relatively new feature. It is possible that there are some scenarios that are not supported. Please raise issues in the rooibos slack channel, if you spot any anomalies. + ### Asserting mocks Mocks are asserted by invoking `m.assertMocks()` @@ -849,7 +898,7 @@ For example, using a function pointer (.brs): For example, using a function pointer (bs): ``` - m.expectCalled(m.myClass.doWork({"matcher": Rooibos.Matcher.anyArray}), returnValue) + m.expectCalled(m.myClass.doWork({"matcher": rooibos.Matcher.anyArray}), returnValue) ``` And inline: @@ -896,11 +945,6 @@ m.expectCalled(videoService.getVideos, someJson, true) Note, you can also opt to disable the error at the whole test suite level; by setting `m.allowNonExistingMethods = true` in your test suite code. -### Mock limitations -Please note, mocks DO NOT work with globally scoped methods (i.e. subs and functions which are not assigned to an associative array). E.g. if you have a method, which is not accessed via `m.SomeMethod`, or `someObject.SomeMethod`, then you cannot mock it. - -**This is due to be addressed 2123Q4** - ## Integrating with your CI Rooibos does not have special test runners for outputting to files, or uploading to servers. However, that will not stop you integrating with your CI system. @@ -1065,7 +1109,7 @@ end function ### Global test setup -I find it really handy to have my own BaseTestSuite, that extends `Rooibos.BaseTestSuite` and I use that as the base of all my tests. In this way I can easily use common utilities and use common beforeEach/setup for setting things up. +I find it really handy to have my own BaseTestSuite, that extends `rooibos.BaseTestSuite` and I use that as the base of all my tests. In this way I can easily use common utilities and use common beforeEach/setup for setting things up. You can always call the super beforeEach/setup/teardown/etc from your other tests, making it trivial to do global setup functions. diff --git a/framework/src/source/BaseTestSuite.bs b/framework/src/source/BaseTestSuite.bs index 60c9f77a..1a5cabae 100644 --- a/framework/src/source/BaseTestSuite.bs +++ b/framework/src/source/BaseTestSuite.bs @@ -332,7 +332,7 @@ namespace rooibos return false end if try - if Rooibos.Common.isBoolean(expr) + if rooibos.common.isBoolean(expr) if expr return m.fail(msg) end if @@ -362,7 +362,7 @@ namespace rooibos return false end if try - if Rooibos.Common.isBoolean(expr) + if rooibos.common.isBoolean(expr) if not expr return m.fail(msg) end if @@ -394,10 +394,10 @@ namespace rooibos return false end if try - if not Rooibos.Common.eqValues(first, second) + if not rooibos.common.eqValues(first, second) if msg = "" - first_as_string = Rooibos.Common.asString(first, true) - second_as_string = Rooibos.Common.asString(second, true) + first_as_string = rooibos.common.asString(first, true) + second_as_string = rooibos.common.asString(second, true) msg = first_as_string + " != " + second_as_string end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -428,10 +428,10 @@ namespace rooibos return false end if try - if not Rooibos.Common.eqValues(first, second, true) + if not rooibos.common.eqValues(first, second, true) if msg = "" - first_as_string = Rooibos.Common.asString(first) - second_as_string = Rooibos.Common.asString(second) + first_as_string = rooibos.common.asString(first) + second_as_string = rooibos.common.asString(second) msg = first_as_string + " != " + second_as_string end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -462,10 +462,10 @@ namespace rooibos return false end if try - if Rooibos.Common.eqValues(first, second) + if rooibos.common.eqValues(first, second) if msg = "" - first_as_string = Rooibos.Common.asString(first, true) - second_as_string = Rooibos.Common.asString(second, true) + first_as_string = rooibos.common.asString(first, true) + second_as_string = rooibos.common.asString(second, true) msg = first_as_string + " == " + second_as_string end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -498,7 +498,7 @@ namespace rooibos try if value <> invalid if msg = "" - expr_as_string = Rooibos.Common.asString(value, true) + expr_as_string = rooibos.common.asString(value, true) msg = expr_as_string + " <> Invalid" end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -560,7 +560,7 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) + if rooibos.common.isAssociativeArray(array) if not array.DoesExist(key) if msg = "" msg = "Array doesn't have the '" + key + "' key." @@ -599,7 +599,7 @@ namespace rooibos end if try - if Rooibos.Common.isAssociativeArray(array) + if rooibos.common.isAssociativeArray(array) if array.DoesExist(key) if msg = "" msg = "Array has the '" + key + "' key." @@ -638,7 +638,7 @@ namespace rooibos end if try - if Rooibos.Common.isAssociativeArray(array) and Rooibos.Common.isArray(keys) + if rooibos.common.isAssociativeArray(array) and rooibos.common.isArray(keys) for each key in keys if not array.DoesExist(key) if msg = "" @@ -678,7 +678,7 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) and Rooibos.Common.isArray(keys) + if rooibos.common.isAssociativeArray(array) and rooibos.common.isArray(keys) for each key in keys if array.DoesExist(key) if msg = "" @@ -721,9 +721,9 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) or Rooibos.Common.isArray(array) - if not Rooibos.Common.arrayContains(array, value, key) - msg = "Array doesn't have the '" + Rooibos.Common.asString(value, true) + "' value." + if rooibos.common.isAssociativeArray(array) or rooibos.common.isArray(array) + if not rooibos.common.arrayContains(array, value, key) + msg = "Array doesn't have the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -757,27 +757,27 @@ namespace rooibos return false end if try - if not Rooibos.Common.isArray(values) + if not rooibos.common.isArray(values) msg = "values to search for are not an Array." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if - if Rooibos.Common.isArray(array) + if rooibos.common.isArray(array) for each value in values isMatched = false - if not Rooibos.Common.isAssociativeArray(value) - msg = "Value to search for was not associativeArray " + Rooibos.Common.asString(value, true) + if not rooibos.common.isAssociativeArray(value) + msg = "Value to search for was not associativeArray " + rooibos.common.asString(value, true) m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if for each item in array - if Rooibos.Common.IsAssociativeArray(item) + if rooibos.common.IsAssociativeArray(item) isValueMatched = true for each key in value fieldValue = value[key] itemValue = item[key] - if not Rooibos.Common.eqValues(fieldValue, itemValue) + if not rooibos.common.eqValues(fieldValue, itemValue) isValueMatched = false exit for end if @@ -790,7 +790,7 @@ namespace rooibos end for ' items in array if not isMatched - msg = "array missing value: " + Rooibos.Common.asString(value, true) + msg = "array missing value: " + rooibos.common.asString(value, true) m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -828,9 +828,9 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) or Rooibos.Common.isArray(array) - if Rooibos.Common.arrayContains(array, value, key) - msg = "Array has the '" + Rooibos.Common.asString(value, true) + "' value." + if rooibos.common.isAssociativeArray(array) or rooibos.common.isArray(array) + if rooibos.common.arrayContains(array, value, key) + msg = "Array has the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -864,8 +864,8 @@ namespace rooibos return false end if try - if (Rooibos.Common.isAssociativeArray(array) and Rooibos.Common.isAssociativeArray(subset)) or (Rooibos.Common.isArray(array) and Rooibos.Common.isArray(subset)) - isAA = Rooibos.Common.isAssociativeArray(subset) + if (rooibos.common.isAssociativeArray(array) and rooibos.common.isAssociativeArray(subset)) or (rooibos.common.isArray(array) and rooibos.common.isArray(subset)) + isAA = rooibos.common.isAssociativeArray(subset) for each item in subset key = invalid value = item @@ -873,8 +873,8 @@ namespace rooibos key = item value = subset[key] end if - if not Rooibos.Common.arrayContains(array, value, key) - msg = "Array doesn't have the '" + Rooibos.Common.asString(value, true) + "' value." + if not rooibos.common.arrayContains(array, value, key) + msg = "Array doesn't have the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -909,8 +909,8 @@ namespace rooibos return false end if try - if (Rooibos.Common.isAssociativeArray(array) and Rooibos.Common.isAssociativeArray(subset)) or (Rooibos.Common.isArray(array) and Rooibos.Common.isArray(subset)) - isAA = Rooibos.Common.isAssociativeArray(subset) + if (rooibos.common.isAssociativeArray(array) and rooibos.common.isAssociativeArray(subset)) or (rooibos.common.isArray(array) and rooibos.common.isArray(subset)) + isAA = rooibos.common.isAssociativeArray(subset) for each item in subset key = invalid value = item @@ -918,8 +918,8 @@ namespace rooibos key = item value = item[key] end if - if Rooibos.Common.arrayContains(array, value, key) - msg = "Array has the '" + Rooibos.Common.asString(value, true) + "' value." + if rooibos.common.arrayContains(array, value, key) + msg = "Array has the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -954,9 +954,9 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) or Rooibos.Common.isArray(array) + if rooibos.common.isAssociativeArray(array) or rooibos.common.isArray(array) if array.Count() <> count - msg = "Array items count " + Rooibos.Common.asString(array.Count()) + " <> " + Rooibos.Common.asString(count) + "." + msg = "Array items count " + rooibos.common.asString(array.Count()) + " <> " + rooibos.common.asString(count) + "." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -990,9 +990,9 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) or Rooibos.Common.isArray(array) + if rooibos.common.isAssociativeArray(array) or rooibos.common.isArray(array) if array.Count() = count - msg = "Array items count = " + Rooibos.Common.asString(count) + "." + msg = "Array items count = " + rooibos.common.asString(count) + "." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1025,15 +1025,15 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(item) or Rooibos.Common.isArray(item) + if rooibos.common.isAssociativeArray(item) or rooibos.common.isArray(item) if item.count() > 0 msg = "Array is not empty." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if - else if Rooibos.Common.isString(item) - if Rooibos.Common.asString(item) <> "" - msg = "String is not empty, contains: " + Rooibos.Common.asString(item, true) + else if rooibos.common.isString(item) + if rooibos.common.asString(item) <> "" + msg = "String is not empty, contains: " + rooibos.common.asString(item, true) m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1066,13 +1066,13 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(item) or Rooibos.Common.isArray(item) + if rooibos.common.isAssociativeArray(item) or rooibos.common.isArray(item) if item.count() = 0 msg = "Array is empty." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if - else if Rooibos.Common.isString(item) + else if rooibos.common.isString(item) if item = "" msg = "Input value is empty." m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -1115,13 +1115,13 @@ namespace rooibos return false end if - if Rooibos.Common.isAssociativeArray(array) or Rooibos.Common.isArray(array) + if rooibos.common.isAssociativeArray(array) or rooibos.common.isArray(array) methodName = "Rooibos_Common_Is" + typeStr typeCheckFunction = m.getIsTypeFunction(methodName) if typeCheckFunction <> invalid for each item in array if not typeCheckFunction(item) - msg = Rooibos.Common.asString(item, true) + " is not a '" + typeStr + "' type." + msg = rooibos.common.asString(item, true) + " is not a '" + typeStr + "' type." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1147,35 +1147,35 @@ namespace rooibos function getIsTypeFunction(name) if name = "Rooibos_Common_IsFunction" - return Rooibos.Common.isFunction + return rooibos.common.isFunction else if name = "Rooibos_Common_IsXmlElement" - return Rooibos.Common.isXmlElement + return rooibos.common.isXmlElement else if name = "Rooibos_Common_IsInteger" - return Rooibos.Common.isInteger + return rooibos.common.isInteger else if name = "Rooibos_Common_IsBoolean" - return Rooibos.Common.isBoolean + return rooibos.common.isBoolean else if name = "Rooibos_Common_IsFloat" - return Rooibos.Common.isFloat + return rooibos.common.isFloat else if name = "Rooibos_Common_IsDouble" - return Rooibos.Common.isDouble + return rooibos.common.isDouble else if name = "Rooibos_Common_IsLongInteger" - return Rooibos.Common.isLongInteger + return rooibos.common.isLongInteger else if name = "Rooibos_Common_IsNumber" - return Rooibos.Common.isNumber + return rooibos.common.isNumber else if name = "Rooibos_Common_IsList" - return Rooibos.Common.isList + return rooibos.common.isList else if name = "Rooibos_Common_IsArray" - return Rooibos.Common.isArray + return rooibos.common.isArray else if name = "Rooibos_Common_IsAssociativeArray" - return Rooibos.Common.isAssociativeArray + return rooibos.common.isAssociativeArray else if name = "Rooibos_Common_IsSGNode" - return Rooibos.Common.isSGNode + return rooibos.common.isSGNode else if name = "Rooibos_Common_IsString" - return Rooibos.Common.isString + return rooibos.common.isString else if name = "Rooibos_Common_IsDateTime" - return Rooibos.Common.isDateTime + return rooibos.common.isDateTime else if name = "Rooibos_Common_IsUndefined" - return Rooibos.Common.isUndefined + return rooibos.common.isUndefined else return invalid end if @@ -1199,7 +1199,7 @@ namespace rooibos try if type(value) <> typeStr if msg = "" - expr_as_string = Rooibos.Common.asString(value, true) + expr_as_string = rooibos.common.asString(value, true) msg = expr_as_string + " was not expected type " + typeStr end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -1232,14 +1232,14 @@ namespace rooibos try if type(value) <> "roSGNode" if msg = "" - expr_as_string = Rooibos.Common.asString(value, true) + expr_as_string = rooibos.common.asString(value, true) msg = expr_as_string + " was not a node, so could not match subtype " + typeStr end if m.currentResult.fail(msg, m.currentAssertLineNumber) return false else if value.subType() <> typeStr if msg = "" - expr_as_string = Rooibos.Common.asString(value, true) + expr_as_string = rooibos.common.asString(value, true) msg = expr_as_string + "( type : " + value.subType() + ") was not of subType " + typeStr end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -1258,13 +1258,13 @@ namespace rooibos if m.currentResult.isFail return false end if - if rooibos.Common.isFunction(func) + if rooibos.common.isFunction(func) func = func.toStr().mid(10).replace("_", ".") end if try - if not Rooibos.Common.isAssociativeArray(value) + if not rooibos.common.isAssociativeArray(value) if msg = "" - expr_as_string = Rooibos.Common.asString(value) + expr_as_string = rooibos.common.asString(value) msg = expr_as_string + " was not an aa, so could not match Class " + func end if m.currentResult.fail(msg, m.currentAssertLineNumber) @@ -1316,7 +1316,7 @@ namespace rooibos childCount = node.getChildCount() end if if childCount <> count - msg = "node items count <> " + Rooibos.Common.asString(count) + ". Received " + Rooibos.Common.asString(childCount) + msg = "node items count <> " + rooibos.common.asString(count) + ". Received " + rooibos.common.asString(childCount) m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1358,7 +1358,7 @@ namespace rooibos childCount = node.getChildCount() end if if childCount = count - msg = "node items count = " + Rooibos.Common.asString(count) + "." + msg = "node items count = " + rooibos.common.asString(count) + "." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1460,8 +1460,8 @@ namespace rooibos end if try if type(node) = "roSGNode" - if not Rooibos.Common.nodeContains(node, value) - msg = "Node doesn't have the '" + Rooibos.Common.asString(value, true) + "' value." + if not rooibos.common.nodeContains(node, value) + msg = "Node doesn't have the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1496,8 +1496,8 @@ namespace rooibos end if try if type(node) = "roSGNode" - if not Rooibos.Common.nodeContains(node, value) - msg = "Node doesn't have the '" + Rooibos.Common.asString(value, true) + "' value." + if not rooibos.common.nodeContains(node, value) + msg = "Node doesn't have the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false else @@ -1544,8 +1544,8 @@ namespace rooibos end if try if type(node) = "roSGNode" - if Rooibos.Common.nodeContains(node, value) - msg = "Node has the '" + Rooibos.Common.asString(value, true) + "' value." + if rooibos.common.nodeContains(node, value) + msg = "Node has the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1579,15 +1579,15 @@ namespace rooibos return false end if try - if (type(node) = "roSGNode" and Rooibos.Common.isAssociativeArray(subset)) or (type(node) = "roSGNode" and Rooibos.Common.isArray(subset)) - isIgnoredFields = Rooibos.Common.isArray(ignoredFields) + if (type(node) = "roSGNode" and rooibos.common.isAssociativeArray(subset)) or (type(node) = "roSGNode" and rooibos.common.isArray(subset)) + isIgnoredFields = rooibos.common.isArray(ignoredFields) for each key in subset if key <> "" - if not isIgnoredFields or not Rooibos.Common.arrayContains(ignoredFields, key) + if not isIgnoredFields or not rooibos.common.arrayContains(ignoredFields, key) subsetValue = subset[key] nodeValue = node[key] - if not Rooibos.Common.eqValues(nodeValue, subsetValue) - msg = key + ": Expected '" + Rooibos.Common.asString(subsetValue, true) + "', got '" + Rooibos.Common.asString(nodeValue, true) + "'" + if not rooibos.common.eqValues(nodeValue, subsetValue) + msg = key + ": Expected '" + rooibos.common.asString(subsetValue, true) + "', got '" + rooibos.common.asString(nodeValue, true) + "'" m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1626,8 +1626,8 @@ namespace rooibos return false end if try - if (type(node) = "roSGNode" and Rooibos.Common.isAssociativeArray(subset)) or (type(node) = "roSGNode" and Rooibos.Common.isArray(subset)) - isAA = Rooibos.Common.isAssociativeArray(subset) + if (type(node) = "roSGNode" and rooibos.common.isAssociativeArray(subset)) or (type(node) = "roSGNode" and rooibos.common.isArray(subset)) + isAA = rooibos.common.isAssociativeArray(subset) for each item in subset key = invalid value = item @@ -1635,8 +1635,8 @@ namespace rooibos key = item value = item[key] end if - if Rooibos.Common.nodeContains(node, value) - msg = "Node has the '" + Rooibos.Common.asString(value, true) + "' value." + if rooibos.common.nodeContains(node, value) + msg = "Node has the '" + rooibos.common.asString(value, true) + "' value." m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1676,15 +1676,15 @@ namespace rooibos return false end if try - if Rooibos.Common.isAssociativeArray(array) and Rooibos.Common.isAssociativeArray(subset) - isIgnoredFields = Rooibos.Common.isArray(ignoredFields) + if rooibos.common.isAssociativeArray(array) and rooibos.common.isAssociativeArray(subset) + isIgnoredFields = rooibos.common.isArray(ignoredFields) for each key in subset if key <> "" - if not isIgnoredFields or not Rooibos.Common.arrayContains(ignoredFields, key) + if not isIgnoredFields or not rooibos.common.arrayContains(ignoredFields, key) subsetValue = subset[key] arrayValue = array[key] - if not Rooibos.Common.eqValues(arrayValue, subsetValue) - msg = key + ": Expected '" + Rooibos.Common.asString(subsetValue, true) + "', got '" + Rooibos.Common.asString(arrayValue, true) + "'" + if not rooibos.common.eqValues(arrayValue, subsetValue) + msg = key + ": Expected '" + rooibos.common.asString(subsetValue, true) + "', got '" + rooibos.common.asString(arrayValue, true) + "'" m.currentResult.fail(msg, m.currentAssertLineNumber) return false end if @@ -1725,7 +1725,10 @@ namespace rooibos ' * @returns {Object} - stub that was wired into the real method ' */ function stub(target, methodName, returnValue = invalid, allowNonExistingMethods = false) as object - if type(target) <> "roAssociativeArray" + if target = invalid and not rooibos.common.isFunction(methodName) + m.fail("could not create Stub. Global function", methodName, ", is invalid") + return {} + else if type(target) <> "roAssociativeArray" m.fail("could not create Stub provided target was null") return {} end if @@ -1746,18 +1749,22 @@ namespace rooibos fake = m.createFake(id, target, methodName, 1, invalid, returnValue) m.stubs[id] = fake - allowNonExisting = m.allowNonExistingMethodsOnMocks = true or allowNonExistingMethods - isMethodPresent = type(target[methodName]) = "Function" or type(target[methodName]) = "roFunction" - if isMethodPresent or allowNonExisting - target[methodName] = m["StubCallback" + id] - target.__stubs = m.stubs - - ' FIXME: add a log setting for this - and add better detection so that stubs know that they are colliding/don't exist/have correct sigs - ' if not isMethodPresent - ' ? "WARNING - stubbing call " ; methodName; " which did not exist on target object" - ' end if + if target.isGlobalCall = true + rooibos.getMocksByFunctionName()[methodName] = fake else - ? "ERROR - could not create Stub : method not found "; target ; "." ; methodName + allowNonExisting = m.allowNonExistingMethodsOnMocks = true or allowNonExistingMethods + isMethodPresent = type(target[methodName]) = "Function" or type(target[methodName]) = "roFunction" + if isMethodPresent or allowNonExisting + target[methodName] = m["StubCallback" + id] + target.__stubs = m.stubs + + ' FIXME: add a log setting for this - and add better detection so that stubs know that they are colliding/don't exist/have correct sigs + ' if not isMethodPresent + ' ? "WARNING - stubbing call " ; methodName; " which did not exist on target object" + ' end if + else + ? "ERROR - could not create Stub : method not found "; target ; "." ; methodName + end if end if return fake @@ -1780,7 +1787,7 @@ namespace rooibos function _expectCalled(target, methodName, rootObject = invalid as dynamic, fullPath = invalid as dynamic, expectedArgs = invalid, returnValue = invalid as dynamic) as object try if type(target) <> "roAssociativeArray" and fullPath <> invalid - target = rooibos.Common.makePathStubbable(rootObject, fullPath) + target = rooibos.common.makePathStubbable(rootObject, fullPath) end if return m.mock(target, methodName, 1, expectedArgs, returnValue, true) catch error @@ -1798,7 +1805,7 @@ namespace rooibos function _stubCall(target, methodName, rootObject = invalid as dynamic, fullPath = invalid as dynamic, returnValue = invalid as dynamic) as object try if type(target) <> "roAssociativeArray" and fullPath <> invalid - target = rooibos.Common.makePathStubbable(rootObject, fullPath) + target = rooibos.common.makePathStubbable(rootObject, fullPath) end if return m.stub(target, methodName, returnValue, true) catch error @@ -1817,7 +1824,7 @@ namespace rooibos function _expectNotCalled(target, methodName, rootObject = invalid as dynamic, fullPath = invalid as dynamic) as object try if type(target) <> "roAssociativeArray" and fullPath <> invalid - target = rooibos.Common.makePathStubbable(rootObject, fullPath) + target = rooibos.common.makePathStubbable(rootObject, fullPath) end if return m.mock(target, methodName, 0, invalid, invalid, true) catch error @@ -1843,13 +1850,18 @@ namespace rooibos ' * @returns {Object} - mock that was wired into the real method ' */ function expectOnce(target, methodName, expectedArgs = invalid, returnValue = invalid, allowNonExistingMethods = false) as object - try + 'HACK + 'HACK + 'HACK + 'HACK + 'HACK + ' try return m.mock(target, methodName, 1, expectedArgs, returnValue, allowNonExistingMethods) - catch error + ' catch error 'bs:disable-next-line - m.currentResult.fail("Setting up mock failed: " + error.message, m.currentAssertLineNumber) - return false - end try + ' m.currentResult.fail("Setting up mock failed: " + error.message, m.currentAssertLineNumber) + ' return false + ' end try return false end function @@ -1943,19 +1955,20 @@ namespace rooibos function mock(target, methodName, expectedInvocations = 1, expectedArgs = invalid, returnValue = invalid, allowNonExistingMethods = false) as object lineNumber = m.currentAssertLineNumber 'check params - if not Rooibos.Common.isAssociativeArray(target) + + if target <> invalid and not rooibos.common.isFunction(target) and not rooibos.common.isAssociativeArray(target) methodName = "" - m.mockFail(lineNumber, "", "mock args: target was not an AA") - else if not Rooibos.Common.isString(methodName) + m.mockFail(lineNumber, "", "mock args: target should be an AA or in-scope Global function", methodName) + else if not rooibos.common.isString(methodName) methodName = "" m.mockFail(lineNumber, "", "mock args: methodName was not a string") - else if not Rooibos.Common.isNumber(expectedInvocations) + else if not rooibos.common.isNumber(expectedInvocations) m.mockFail(lineNumber, methodName, "mock args: expectedInvocations was not an int") - else if not Rooibos.Common.isArray(expectedArgs) and Rooibos.Common.isValid(expectedArgs) + else if not rooibos.common.isArray(expectedArgs) and rooibos.common.isValid(expectedArgs) m.mockFail(lineNumber, methodName, "mock args: expectedArgs was not invalid or an array of args") - else if Rooibos.Common.isUndefined(expectedArgs) + else if rooibos.common.isUndefined(expectedArgs) m.mockFail(lineNumber, methodName, "mock args: expectedArgs undefined") - else if Rooibos.Common.isUndefined(returnValue) + else if rooibos.common.isUndefined(returnValue) m.mockFail(lineNumber, methodName, "mock args: returnValue undefined") end if @@ -1968,9 +1981,15 @@ namespace rooibos m.__mockId = -1 m.__mockTargetId = -1 m.mocks = {} + rooibos.resetMocksByFunctionName() end if fake = invalid + if rooibos.common.isFunction(target) + target = { + isGlobalCall: true + } + end if if not target.doesExist("__rooibosTargetId") m.__mockTargetId++ target["__rooibosTargetId"] = m.__mockTargetId @@ -1979,7 +1998,7 @@ namespace rooibos for i = 0 to m.__mockId id = stri(i).trim() mock = m.mocks[id] - if mock <> invalid and mock.methodName = methodName and mock.target.__rooibosTargetId = target.__rooibosTargetId + if mock <> invalid and mock.methodName = methodName and (mock.target.__rooibosTargetId = target.__rooibosTargetId or (mock.target.isGlobalCall = true and target.isGlobalCall = true)) fake = mock fake.lineNumbers.push(lineNumber) exit for @@ -1996,21 +2015,26 @@ namespace rooibos fake = m.createFake(id, target, methodName, expectedInvocations, expectedArgs, returnValue) m.mocks[id] = fake 'this will bind it to m - allowNonExisting = m.allowNonExistingMethodsOnMocks = true or allowNonExistingMethods - isMethodPresent = type(target[methodName]) = "Function" or type(target[methodName]) = "roFunction" - if isMethodPresent or allowNonExisting - target[methodName] = m["MockCallback" + id] - target.__mocks = m.mocks - - if not isMethodPresent - ' ? "WARNING - mocking call " ; methodName; " which did not exist on target object" - end if + if target.isGlobalCall = true + rooibos.getMocksByFunctionName()[methodName] = fake else - ? "ERROR - could not create Mock : method not found "; target ; "." ; methodName + allowNonExisting = m.allowNonExistingMethodsOnMocks = true or allowNonExistingMethods + isMethodPresent = type(target[methodName]) = "Function" or type(target[methodName]) = "roFunction" + if isMethodPresent or allowNonExisting + target[methodName] = m["MockCallback" + id] + target.__mocks = m.mocks + + if not isMethodPresent + ' ? "WARNING - mocking call " ; methodName; " which did not exist on target object" + end if + else + ? "ERROR - could not create Mock : method not found "; target ; "." ; methodName + end if end if else m.combineFakes(fake, m.createFake(id, target, methodName, expectedInvocations, expectedArgs, returnValue)) end if + return fake end function @@ -2030,22 +2054,23 @@ namespace rooibos function createFake(id, target, methodName, expectedInvocations = 1, expectedArgs = invalid, returnValue = invalid) as object expectedArgsValues = [] lineNumber = m.currentAssertLineNumber - hasArgs = Rooibos.Common.isArray(expectedArgs) + hasArgs = rooibos.common.isArray(expectedArgs) defaultValue = m.ignoreValue if hasArgs defaultValue = m.invalidValue else expectedArgs = [] end if + lineNumbers = [lineNumber] for i = 0 to 9 if hasArgs and expectedArgs.count() > i 'guard against bad values value = expectedArgs[i] - if not Rooibos.Common.isUndefined(value) - if Rooibos.Common.isAssociativeArray(value) and Rooibos.Common.isValid(value.matcher) - if not Rooibos.Common.isFunction(value.matcher) + if not rooibos.common.isUndefined(value) + if rooibos.common.isAssociativeArray(value) and rooibos.common.isValid(value.matcher) + if not rooibos.common.isFunction(value.matcher) ? "[ERROR] you have specified a matching function; but it is not in scope!" expectedArgsValues.push("#ERR-OUT_OF_SCOPE_MATCHER!") else @@ -2158,7 +2183,7 @@ namespace rooibos ' * @description Will check all mocks that have been created to ensure they were invoked the expected amount of times, with the expected args. ' */ function assertMocks() as void - if m.__mockId = invalid or not Rooibos.Common.isAssociativeArray(m.mocks) + if m.__mockId = invalid or not rooibos.common.isAssociativeArray(m.mocks) return end if @@ -2169,8 +2194,8 @@ namespace rooibos m.mockFail(mock.lineNumbers[0], methodName, "Wrong number of calls. (" + stri(mock.invocations).trim() + " / " + stri(mock.expectedInvocations).trim() + ")") m.cleanMocks() return - else if mock.expectedInvocations > 0 and (Rooibos.Common.isArray(mock.expectedArgs) or (type(mock.expectedArgs) = "roAssociativeArray" and Rooibos.Common.isArray(mock.expectedArgs.multiInvoke))) - isMultiArgsSupported = type(mock.expectedArgs) = "roAssociativeArray" and Rooibos.Common.isArray(mock.expectedArgs.multiInvoke) + else if mock.expectedInvocations > 0 and (rooibos.common.isArray(mock.expectedArgs) or (type(mock.expectedArgs) = "roAssociativeArray" and rooibos.common.isArray(mock.expectedArgs.multiInvoke))) + isMultiArgsSupported = type(mock.expectedArgs) = "roAssociativeArray" and rooibos.common.isArray(mock.expectedArgs.multiInvoke) for invocationIndex = 0 to mock.invocations - 1 invokedArgs = mock.allInvokedArgs[invocationIndex] @@ -2182,25 +2207,25 @@ namespace rooibos for i = 0 to expectedArgs.count() - 1 value = invokedArgs[i] expected = expectedArgs[i] - didNotExpectArg = Rooibos.Common.isString(expected) and expected = m.invalidValue + didNotExpectArg = rooibos.common.isString(expected) and expected = m.invalidValue if didNotExpectArg expected = invalid end if - isUsingMatcher = Rooibos.Common.isAssociativeArray(expected) and Rooibos.Common.isFunction(expected.matcher) + isUsingMatcher = rooibos.common.isAssociativeArray(expected) and rooibos.common.isFunction(expected.matcher) if isUsingMatcher if not expected.matcher(value) - m.mockFail(mock.lineNumbers[invocationIndex], methodName, "on Invocation #" + stri(invocationIndex).trim() + ", expected arg #" + stri(i).trim() + " to match matching function '" + Rooibos.Common.asString(expected.matcher) + "' got '" + Rooibos.Common.asString(value, true) + "')") + m.mockFail(mock.lineNumbers[invocationIndex], methodName, "on Invocation #" + stri(invocationIndex).trim() + ", expected arg #" + stri(i).trim() + " to match matching function '" + rooibos.common.asString(expected.matcher) + "' got '" + rooibos.common.asString(value, true) + "')") m.cleanMocks() end if else - if not (Rooibos.Common.isString(expected) and expected = m.ignoreValue) and not Rooibos.Common.eqValues(value, expected) + if not (rooibos.common.isString(expected) and expected = m.ignoreValue) and not rooibos.common.eqValues(value, expected) if expected = invalid expected = "[INVALID]" end if - m.mockFail(mock.lineNumbers[invocationIndex], methodName, "on Invocation #" + stri(invocationIndex).trim() + ", expected arg #" + stri(i).trim() + " to be '" + Rooibos.Common.asString(expected, true) + "' got '" + Rooibos.Common.asString(value, true) + "')") + m.mockFail(mock.lineNumbers[invocationIndex], methodName, "on Invocation #" + stri(invocationIndex).trim() + ", expected arg #" + stri(i).trim() + " to be '" + rooibos.common.asString(expected, true) + "' got '" + rooibos.common.asString(value, true) + "')") m.cleanMocks() return end if @@ -2229,6 +2254,7 @@ namespace rooibos mock.target.__mocks = invalid end for m.mocks = invalid + rooibos.resetMocksByFunctionName() end function ' /** @@ -2247,6 +2273,7 @@ namespace rooibos stub.target.__stubs = invalid end for m.stubs = invalid + rooibos.resetMocksByFunctionName() end function @@ -2699,4 +2726,23 @@ namespace rooibos ' ? "++++++++ TEST TIMED OUT" m.testRunner.currentTestSuite.failBecauseOfTimeOut() end function + + function getMocksByFunctionName() + if m._rMocksByFunctionName = invalid + m._rMocksByFunctionName = {} + end if + return m._rMocksByFunctionName + end function + + function resetMocksByFunctionName() + m._rMocksByFunctionName = invalid + end function + + function getMockForFunction(functionName as string) + return rooibos.getMocksByFunctionName()[functionName] + end function + + function isFunctionMocked(functionName as string) + return rooibos.getMockForFunction(functionName) <> invalid + end function end namespace \ No newline at end of file diff --git a/framework/src/source/CommonUtils.bs b/framework/src/source/CommonUtils.bs index dd25db8d..9b9a6f51 100755 --- a/framework/src/source/CommonUtils.bs +++ b/framework/src/source/CommonUtils.bs @@ -1,4 +1,4 @@ -namespace rooibos.Common +namespace rooibos.common ' /** ' * @module CommonUtils ' */ @@ -15,7 +15,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains XMLElement interface, else return false ' */ function isXmlElement(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifXMLElement") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifXMLElement") <> invalid end function ' /** @@ -27,12 +27,12 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains Function interface, else return false ' */ function isFunction(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifFunction") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifFunction") <> invalid end function ' /** - ' * @name Rooibos.Common.getFunction + ' * @name rooibos.common.getFunction ' * @function ' * @description looks up the function by name, for the function map ' * @memberof module:CommonUtils @@ -41,10 +41,10 @@ namespace rooibos.Common ' * @returns {Function} - function pointer or invalid ' */ function getFunction(filename, functionName) as object - if not Rooibos.Common.isNotEmptyString(functionName) + if not rooibos.common.isNotEmptyString(functionName) return invalid end if - if not Rooibos.Common.isNotEmptyString(filename) + if not rooibos.common.isNotEmptyString(filename) return invalid end if 'bs:disable-next-line @@ -68,7 +68,7 @@ namespace rooibos.Common end function ' /** - ' * @name Rooibos.Common.getFunctionBruteforce + ' * @name rooibos.common.getFunctionBruteforce ' * @function ' * @description looks up the function by name, from any function map ' * in future @@ -78,7 +78,7 @@ namespace rooibos.Common ' * @returns {Function} - function pointer or invalid ' */ function getFunctionBruteForce(functionName) as object - if not Rooibos.Common.isNotEmptyString(functionName) + if not rooibos.common.isNotEmptyString(functionName) return invalid end if 'bs:disable-next-line @@ -116,7 +116,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains Boolean interface, else return false ' */ function isBoolean(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifBoolean") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifBoolean") <> invalid end function ' /** @@ -128,7 +128,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value type equals Integer, else return false ' */ function isInteger(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifInt") <> invalid and (Type(value) = "roInt" or Type(value) = "roInteger" or Type(value) = "Integer") + return rooibos.common.isValid(value) and GetInterface(value, "ifInt") <> invalid and (Type(value) = "roInt" or Type(value) = "roInteger" or Type(value) = "Integer") end function ' /** @@ -140,7 +140,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains Float interface, else return false ' */ function isFloat(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifFloat") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifFloat") <> invalid end function ' /** @@ -152,7 +152,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains Double interface, else return false ' */ function isDouble(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifDouble") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifDouble") <> invalid end function ' /** @@ -164,7 +164,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains LongInteger interface, else return false ' */ function isLongInteger(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifLongInt") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifLongInt") <> invalid end function ' /** @@ -176,7 +176,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value is number, else return false ' */ function isNumber(value) as boolean - return Rooibos.Common.isLongInteger(value) or Rooibos.Common.isDouble(value) or Rooibos.Common.isInteger(value) or Rooibos.Common.isFloat(value) + return rooibos.common.isLongInteger(value) or rooibos.common.isDouble(value) or rooibos.common.isInteger(value) or rooibos.common.isFloat(value) end function ' /** @@ -188,7 +188,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains List interface, else return false ' */ function isList(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifList") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifList") <> invalid end function ' /** @@ -200,7 +200,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains Array interface, else return false ' */ function isArray(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifArray") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifArray") <> invalid end function ' /** @@ -212,7 +212,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains AssociativeArray interface, else return false ' */ function isAssociativeArray(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifAssociativeArray") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifAssociativeArray") <> invalid end function ' /** @@ -224,7 +224,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains SGNodeChildren interface, else return false ' */ function isSGNode(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifSGNodeChildren") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifSGNodeChildren") <> invalid end function ' /** @@ -236,7 +236,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains String interface, else return false ' */ function isString(value) as boolean - return Rooibos.Common.isValid(value) and GetInterface(value, "ifString") <> invalid + return rooibos.common.isValid(value) and GetInterface(value, "ifString") <> invalid end function ' /** @@ -248,7 +248,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains String interface and length more 0, else return false ' */ function isNotEmptyString(value) as boolean - return Rooibos.Common.isString(value) and len(value) > 0 + return rooibos.common.isString(value) and len(value) > 0 end function ' /** @@ -260,7 +260,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value contains DateTime interface, else return false ' */ function isDateTime(value) as boolean - return Rooibos.Common.isValid(value) and (GetInterface(value, "ifDateTime") <> invalid or Type(value) = "roDateTime") + return rooibos.common.isValid(value) and (GetInterface(value, "ifDateTime") <> invalid or Type(value) = "roDateTime") end function ' /** @@ -272,7 +272,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value initialized and not equal invalid, else return false ' */ function isValid(value) as boolean - return not Rooibos.Common.isUndefined(value) and value <> invalid + return not rooibos.common.isUndefined(value) and value <> invalid end function function isUndefined(value) as boolean @@ -304,23 +304,23 @@ namespace rooibos.Common ' * @returns {String} - converted string ' */ function asString(input, includeType = false) as string - if Rooibos.Common.isValid(input) = false + if rooibos.common.isValid(input) = false return "INVALID" - else if Rooibos.Common.isString(input) + else if rooibos.common.isString(input) if includeType return """" + input + """" else return input end if - else if Rooibos.Common.isInteger(input) or Rooibos.Common.isLongInteger(input) or Rooibos.Common.isBoolean(input) + else if rooibos.common.isInteger(input) or rooibos.common.isLongInteger(input) or rooibos.common.isBoolean(input) if includeType - return input.ToStr() + " (" + Rooibos.Common.getSafeType(input) + ")" + return input.ToStr() + " (" + rooibos.common.getSafeType(input) + ")" else return input.ToStr() end if - else if Rooibos.Common.isFloat(input) or Rooibos.Common.isDouble(input) + else if rooibos.common.isFloat(input) or rooibos.common.isDouble(input) if includeType - return Str(input).Trim() + " (" + Rooibos.Common.getSafeType(input) + ")" + return Str(input).Trim() + " (" + rooibos.common.getSafeType(input) + ")" else return Str(input).Trim() end if @@ -335,19 +335,19 @@ namespace rooibos.Common isFirst = false end if for each key in input - if rooibos.Common.canSafelyIterateAAKey(input, key) - text = text + key + ":" + Rooibos.Common.asString(input[key], includeType) + if rooibos.common.canSafelyIterateAAKey(input, key) + text = text + key + ":" + rooibos.common.asString(input[key], includeType) end if end for text = text + "}" return text - else if Rooibos.Common.isArray(input) + else if rooibos.common.isArray(input) text = "[" join = "" maxLen = 500 for each v in input if len(text) < maxLen - text += join + rooibos.Common.asString(v, includeType) + text += join + rooibos.common.asString(v, includeType) join = ", " end if end for @@ -356,7 +356,7 @@ namespace rooibos.Common end if text = text + "]" return text - else if Rooibos.Common.isFunction(input) + else if rooibos.common.isFunction(input) return input.toStr() + "(function)" else return "" @@ -372,13 +372,13 @@ namespace rooibos.Common ' * @returns {Integer} - converted Integer ' */ function asInteger(input) as integer - if Rooibos.Common.isValid(input) = false + if rooibos.common.isValid(input) = false return 0 - else if Rooibos.Common.isString(input) + else if rooibos.common.isString(input) return input.ToInt() - else if Rooibos.Common.isInteger(input) + else if rooibos.common.isInteger(input) return input - else if Rooibos.Common.isFloat(input) or Rooibos.Common.isDouble(input) or Rooibos.Common.isLongInteger(input) + else if rooibos.common.isFloat(input) or rooibos.common.isDouble(input) or rooibos.common.isLongInteger(input) return Int(input) else return 0 @@ -394,11 +394,11 @@ namespace rooibos.Common ' * @returns {Integer} - converted LongInteger ' */ function asLongInteger(input) as longinteger - if Rooibos.Common.isValid(input) = false + if rooibos.common.isValid(input) = false return 0 - else if Rooibos.Common.isString(input) - return Rooibos.Common.asInteger(input) - else if Rooibos.Common.isLongInteger(input) or Rooibos.Common.isFloat(input) or Rooibos.Common.isDouble(input) or Rooibos.Common.isInteger(input) + else if rooibos.common.isString(input) + return rooibos.common.asInteger(input) + else if rooibos.common.isLongInteger(input) or rooibos.common.isFloat(input) or rooibos.common.isDouble(input) or rooibos.common.isInteger(input) return input else return 0 @@ -414,13 +414,13 @@ namespace rooibos.Common ' * @returns {Float} - converted Float ' */ function asFloat(input) as float - if Rooibos.Common.isValid(input) = false + if rooibos.common.isValid(input) = false return 0.0 - else if Rooibos.Common.isString(input) + else if rooibos.common.isString(input) return input.ToFloat() - else if Rooibos.Common.isInteger(input) + else if rooibos.common.isInteger(input) return (input / 1) - else if Rooibos.Common.isFloat(input) or Rooibos.Common.isDouble(input) or Rooibos.Common.isLongInteger(input) + else if rooibos.common.isFloat(input) or rooibos.common.isDouble(input) or rooibos.common.isLongInteger(input) return input else return 0.0 @@ -436,11 +436,11 @@ namespace rooibos.Common ' * @returns {Float} - converted Double ' */ function asDouble(input) as double - if Rooibos.Common.isValid(input) = false + if rooibos.common.isValid(input) = false return 0.0 - else if Rooibos.Common.isString(input) - return Rooibos.Common.asFloat(input) - else if Rooibos.Common.isInteger(input) or Rooibos.Common.isLongInteger(input) or Rooibos.Common.isFloat(input) or Rooibos.Common.isDouble(input) + else if rooibos.common.isString(input) + return rooibos.common.asFloat(input) + else if rooibos.common.isInteger(input) or rooibos.common.isLongInteger(input) or rooibos.common.isFloat(input) or rooibos.common.isDouble(input) return input else return 0.0 @@ -456,13 +456,13 @@ namespace rooibos.Common ' * @returns {Boolean} - converted boolean ' */ function asBoolean(input) as boolean - if Rooibos.Common.isValid(input) = false + if rooibos.common.isValid(input) = false return false - else if Rooibos.Common.isString(input) + else if rooibos.common.isString(input) return LCase(input) = "true" - else if Rooibos.Common.isInteger(input) or Rooibos.Common.isFloat(input) + else if rooibos.common.isInteger(input) or rooibos.common.isFloat(input) return input <> 0 - else if Rooibos.Common.isBoolean(input) + else if rooibos.common.isBoolean(input) return input else return false @@ -478,8 +478,8 @@ namespace rooibos.Common ' * @returns {Array} - converted array ' */ function asArray(value) as object - if Rooibos.Common.isValid(value) - if not Rooibos.Common.isArray(value) + if rooibos.common.isValid(value) + if not rooibos.common.isArray(value) return [value] else return value @@ -501,10 +501,10 @@ namespace rooibos.Common ' * @returns {Boolean} - true if value is null or empty string, else return false ' */ function isNullOrEmpty(value) as boolean - if Rooibos.Common.isString(value) + if rooibos.common.isString(value) return Len(value) = 0 else - return not Rooibos.Common.isValid(value) + return not rooibos.common.isValid(value) end if end function @@ -524,19 +524,19 @@ namespace rooibos.Common ' * @returns {Integer} - element index if array contains a value, else return -1 ' */ function findElementIndexInArray(array, value, compareAttribute = invalid, caseSensitive = false, callCount = 0) as integer - if callCount = 0 and not Rooibos.Common.isArray(array) - array = Rooibos.Common.asArray(array) + if callCount = 0 and not rooibos.common.isArray(array) + array = rooibos.common.asArray(array) end if - if Rooibos.Common.isArray(array) - for i = 0 to Rooibos.Common.asArray(array).Count() - 1 + if rooibos.common.isArray(array) + for i = 0 to rooibos.common.asArray(array).Count() - 1 compareValue = array[i] - if compareAttribute <> invalid and Rooibos.Common.isAssociativeArray(compareValue) + if compareAttribute <> invalid and rooibos.common.isAssociativeArray(compareValue) compareValue = compareValue.lookupCI(compareAttribute) end if - if Rooibos.Common.eqValues(compareValue, value, callCount + 1) + if rooibos.common.eqValues(compareValue, value, callCount + 1) return i end if next @@ -555,7 +555,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if array contains a value, else return false ' */ function arrayContains(array, value, compareAttribute = invalid) as boolean - return (Rooibos.Common.findElementIndexInArray(array, value, compareAttribute) > -1) + return (rooibos.common.findElementIndexInArray(array, value, compareAttribute) > -1) end function @@ -602,7 +602,7 @@ namespace rooibos.Common ' * @returns {Boolean} - true if node contains a value, else return false ' */ function nodeContains(node, value) as boolean - return (Rooibos.Common.findElementIndexInNode(node, value) > -1) + return (rooibos.common.findElementIndexInNode(node, value) > -1) end function @@ -649,8 +649,8 @@ namespace rooibos.Common end if ' Workaraund for bug with string boxing, and box everything else - val1Type = Rooibos.Common.getSafeType(Value1) - val2Type = Rooibos.Common.getSafeType(Value2) + val1Type = rooibos.common.getSafeType(Value1) + val2Type = rooibos.common.getSafeType(Value2) if val1Type = invalid or val2Type = invalid ? "ERROR!!!! - undefined value passed" return false @@ -669,11 +669,11 @@ namespace rooibos.Common valtype = val1Type if val1Type = "List" - return Rooibos.Common.eqArray(Value1, Value2, fuzzy, callCount + 1) + return rooibos.common.eqArray(Value1, Value2, fuzzy, callCount + 1) else if valtype = "roAssociativeArray" - return Rooibos.Common.eqAssocArray(Value1, Value2, fuzzy, callCount + 1) + return rooibos.common.eqAssocArray(Value1, Value2, fuzzy, callCount + 1) else if valtype = "roArray" - return Rooibos.Common.eqArray(Value1, Value2, fuzzy, callCount + 1) + return rooibos.common.eqArray(Value1, Value2, fuzzy, callCount + 1) else if valtype = "roSGNode" if val2Type <> "roSGNode" return false @@ -682,7 +682,7 @@ namespace rooibos.Common end if else if fuzzy = true - return Rooibos.Common.asString(Value1) = Rooibos.Common.asString(Value2) + return rooibos.common.asString(Value1) = rooibos.common.asString(Value2) else 'If you crashed on this line, then you're trying to compare '2 things which can't be compared - check what value1 and value2 @@ -694,8 +694,8 @@ namespace rooibos.Common end function function eqTypes(Value1, Value2) as dynamic - val1Type = Rooibos.Common.getSafeType(Value1) - val2Type = Rooibos.Common.getSafeType(Value2) + val1Type = rooibos.common.getSafeType(Value1) + val2Type = rooibos.common.getSafeType(Value2) if val1Type = invalid or val2Type = invalid ? "ERROR!!!! - undefined value passed" return false @@ -736,7 +736,7 @@ namespace rooibos.Common if rooibos.common.canSafelyIterateAAKey(Value1, k) and rooibos.common.canSafelyIterateAAKey(Value2, k) v1 = Value1[k] v2 = Value2[k] - if not Rooibos.Common.eqValues(v1, v2, fuzzy, callCount + 1) + if not rooibos.common.eqValues(v1, v2, fuzzy, callCount + 1) return false end if end if @@ -771,7 +771,7 @@ namespace rooibos.Common ? "REACHED MAX ITERATIONS DOING COMPARISON" return true end if - if not (Rooibos.Common.isArray(Value1)) or not Rooibos.Common.isArray(Value2) + if not (rooibos.common.isArray(Value1)) or not rooibos.common.isArray(Value2) return false end if @@ -784,7 +784,7 @@ namespace rooibos.Common for i = 0 to l1 - 1 v1 = Value1[i] v2 = Value2[i] - if not Rooibos.Common.eqValues(v1, v2, fuzzy, callCount + 1) + if not rooibos.common.eqValues(v1, v2, fuzzy, callCount + 1) return false end if end for @@ -838,14 +838,14 @@ namespace rooibos.Common end if nextPart = invalid - if rooibos.Common.isArray(part) and isIndexNumber + if rooibos.common.isArray(part) and isIndexNumber nextPart = part[index] else if type(part) = "roAssociativeArray" and not isIndexNumber nextPart = part[index] end if if nextPart = invalid or type(nextPart) <> "roAssociativeArray" - if (not isIndexNumber and type(part) = "roAssociativeArray") or (isIndexNumber and (rooibos.Common.isArray(part))) + if (not isIndexNumber and type(part) = "roAssociativeArray") or (isIndexNumber and (rooibos.common.isArray(part))) nextPart = { id: index } part[index] = nextPart else diff --git a/framework/src/source/ConsoleTestReporter.bs b/framework/src/source/ConsoleTestReporter.bs index 285d48de..d0e609f4 100644 --- a/framework/src/source/ConsoleTestReporter.bs +++ b/framework/src/source/ConsoleTestReporter.bs @@ -30,13 +30,13 @@ namespace rooibos 'bs:disable-next-line ignoredInfo = m.testRunner.runtimeConfig.getIgnoredTestInfo() - m.printLine(0, "Total: " + Rooibos.Common.AsString(m.allStats.ranCount)) - m.printLine(0, " Passed: " + Rooibos.Common.AsString(m.allStats.passedCount)) - m.printLine(0, " Crashed: " + Rooibos.Common.AsString(m.allStats.crashedCount)) - m.printLine(0, " Failed: " + Rooibos.Common.AsString(m.allStats.failedCount)) + m.printLine(0, "Total: " + rooibos.common.AsString(m.allStats.ranCount)) + m.printLine(0, " Passed: " + rooibos.common.AsString(m.allStats.passedCount)) + m.printLine(0, " Crashed: " + rooibos.common.AsString(m.allStats.crashedCount)) + m.printLine(0, " Failed: " + rooibos.common.AsString(m.allStats.failedCount)) 'bs:disable-next-line - m.printLine(0, " Ignored: " + Rooibos.Common.AsString(ignoredInfo.count)) - m.printLine(0, " Time: " + Rooibos.Common.AsString(m.allStats.time) + "ms") + m.printLine(0, " Ignored: " + rooibos.common.AsString(ignoredInfo.count)) + m.printLine(0, " Time: " + rooibos.common.AsString(m.allStats.time) + "ms") m.printLine() m.printLine() @@ -103,7 +103,7 @@ namespace rooibos insetText = "" if test.isParamTest <> true - messageLine = Rooibos.Common.fillText(" " + testChar + " |--" + test.name + " : ", ".", m.lineWidth) + messageLine = rooibos.common.fillText(" " + testChar + " |--" + test.name + " : ", ".", m.lineWidth) m.printLine(0, messageLine + test.result.getStatusText() + timeText) m.printLine(0, " | " + insetText + " |--Test : " + testLocationText) else if test.paramTestIndex = 0 @@ -130,7 +130,7 @@ namespace rooibos else rawParams = test.rawParams end if - messageLine = Rooibos.Common.fillText(" " + testChar + insetText + " |--" + formatJson(rawParams) + " : ", ".", m.lineWidth) + messageLine = rooibos.common.fillText(" " + testChar + insetText + " |--" + formatJson(rawParams) + " : ", ".", m.lineWidth) m.printLine(0, messageLine + test.result.getStatusText() + timeText) end if diff --git a/framework/src/source/Matchers.bs b/framework/src/source/Matchers.bs index 193056a5..2445b869 100644 --- a/framework/src/source/Matchers.bs +++ b/framework/src/source/Matchers.bs @@ -1,27 +1,27 @@ namespace rooibos.Matcher function anyString(value) - return Rooibos.Common.isString(value) + return rooibos.common.isString(value) end function function anyBool(value) - return Rooibos.Common.isBoolean(value) + return rooibos.common.isBoolean(value) end function function anyNumber(value) - return Rooibos.Common.isNumber(value) + return rooibos.common.isNumber(value) end function function anyAA(value) - return Rooibos.Common.isAssociativeArray(value) + return rooibos.common.isAssociativeArray(value) end function function anyArray(value) - return Rooibos.Common.isArray(value) + return rooibos.common.isArray(value) end function function anyNode(value) - return Rooibos.Common.isSGNode(value) + return rooibos.common.isSGNode(value) end function diff --git a/framework/src/source/RooibosScene.xml b/framework/src/source/RooibosScene.xml index 0dfd684b..5937a1ff 100644 --- a/framework/src/source/RooibosScene.xml +++ b/framework/src/source/RooibosScene.xml @@ -19,7 +19,7 @@ uri='source/rooibos/Matchers.bs' />