diff --git a/bsc-plugin/src/lib/rooibos/Annotation.ts b/bsc-plugin/src/lib/rooibos/Annotation.ts index 23c9d83..dfd3821 100644 --- a/bsc-plugin/src/lib/rooibos/Annotation.ts +++ b/bsc-plugin/src/lib/rooibos/Annotation.ts @@ -219,4 +219,4 @@ export class RooibosAnnotation { export function getAnnotationType(text: string): AnnotationType { return annotationLookup[text.toLowerCase()] || AnnotationType.None; -} \ No newline at end of file +} diff --git a/bsc-plugin/src/lib/rooibos/MockUtil.ts b/bsc-plugin/src/lib/rooibos/MockUtil.ts index fa66034..1462d6c 100644 --- a/bsc-plugin/src/lib/rooibos/MockUtil.ts +++ b/bsc-plugin/src/lib/rooibos/MockUtil.ts @@ -107,7 +107,7 @@ export class MockUtil { ${globalAaName} = getGlobalAa() if RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"] <> invalid ${resultName} = RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"].callback(${paramNames}) - return${requiresReturnValue ? ` ${resultName}` : '' } + return${requiresReturnValue ? ` ${resultName}` : ''} else if type(${globalAaName}?.${storageName}?.${methodName}).endsWith("Function") __stubFunction = ${globalAaName}.${storageName}.${methodName} ${resultName} = __stubFunction(${paramNames}) diff --git a/bsc-plugin/src/lib/rooibos/RooibosConfig.ts b/bsc-plugin/src/lib/rooibos/RooibosConfig.ts index 6d5cd16..af5cdc6 100644 --- a/bsc-plugin/src/lib/rooibos/RooibosConfig.ts +++ b/bsc-plugin/src/lib/rooibos/RooibosConfig.ts @@ -24,7 +24,12 @@ export interface RooibosConfig { catchCrashes?: boolean; throwOnFailedAssertion?: boolean; sendHomeOnFinish?: boolean; + + /** + * @deprecated Use the `reporters` array instead + */ reporter?: string; + reporters?: string[]; keepAppOpen?: boolean; testSceneName?: string; diff --git a/bsc-plugin/src/lib/rooibos/RooibosSession.ts b/bsc-plugin/src/lib/rooibos/RooibosSession.ts index bbbac93..adb0d38 100644 --- a/bsc-plugin/src/lib/rooibos/RooibosSession.ts +++ b/bsc-plugin/src/lib/rooibos/RooibosSession.ts @@ -137,7 +137,7 @@ export class RooibosSession { method.func.body.statements.length, Parser.parse(undent` return { - "reporter": "${this.config.reporter || ''}" + "reporters": ${this.getReportersList()} "failFast": ${this.config.failFast ? 'true' : 'false'} "sendHomeOnFinish": ${this.config.sendHomeOnFinish ? 'true' : 'false'} "logLevel": ${this.config.logLevel ?? 0} @@ -156,6 +156,30 @@ export class RooibosSession { } } + getReportersList() { + let reporters = this.config.reporters; + if (!Array.isArray(reporters)) { + reporters = []; + } + if (this.config.reporter) { + // @todo: warn that `reporter` is deprecated and to use `reporters` instead + reporters.push(this.config.reporter); + } + if (reporters.length < 1) { + reporters.push('console'); + } + return `[${reporters.map(this.sanitiseReporterName).toString()}]`; + } + + sanitiseReporterName(name: string) { + switch (name.toLowerCase()) { + case 'console': return 'rooibos_ConsoleTestReporter'; + case 'junit': return 'rooibos_JUnitTestReporter'; + } + // @todo: check if function name is valid + return name; + } + updateVersionTextFunction(classStatement: ClassStatement, editor: AstEditor) { let method = classStatement.methods.find((m) => m.name.text === 'getVersionText'); if (method) { diff --git a/bsc-plugin/src/lib/rooibos/Utils.ts b/bsc-plugin/src/lib/rooibos/Utils.ts index 39bd00a..7b3d788 100644 --- a/bsc-plugin/src/lib/rooibos/Utils.ts +++ b/bsc-plugin/src/lib/rooibos/Utils.ts @@ -107,7 +107,7 @@ export function getPathValuePartAsString(expr: Expression) { export function getScopeForSuite(testSuite: TestSuite) { if (testSuite.isNodeTest) { - return testSuite.file.program.getScopesForFile(testSuite.file).find((scope)=> { + return testSuite.file.program.getScopesForFile(testSuite.file).find((scope) => { return isXmlScope(scope) && scope.xmlFile.componentName.text === testSuite.generatedNodeName; }); diff --git a/bsc-plugin/src/plugin.spec.ts b/bsc-plugin/src/plugin.spec.ts index c4d6604..3ff66c9 100644 --- a/bsc-plugin/src/plugin.spec.ts +++ b/bsc-plugin/src/plugin.spec.ts @@ -15,21 +15,13 @@ describe('RooibosPlugin', () => { let program: Program; let builder: ProgramBuilder; let plugin: RooibosPlugin; - let options; - beforeEach(() => { - plugin = new RooibosPlugin(); - options = { - rootDir: _rootDir, - stagingFolderPath: _stagingFolderPath, - stagingDir: _stagingFolderPath, - rooibos: { - isGlobalMethodMockingEnabled: true - } - }; + + function setupProgram(options) { fsExtra.ensureDirSync(_stagingFolderPath); fsExtra.ensureDirSync(_rootDir); fsExtra.ensureDirSync(tmpPath); + plugin = new RooibosPlugin(); builder = new ProgramBuilder(); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument builder.options = util.normalizeAndResolveConfig(options); @@ -40,13 +32,28 @@ describe('RooibosPlugin', () => { plugin.beforeProgramCreate(builder); plugin.fileFactory['options'].frameworkSourcePath = path.resolve(path.join('../framework/src/source')); plugin.afterProgramCreate(program); - }); + } - afterEach(() => { + function destroyProgram() { fsExtra.ensureDirSync(tmpPath); fsExtra.emptyDirSync(tmpPath); builder.dispose(); program.dispose(); + } + + beforeEach(() => { + setupProgram({ + rootDir: _rootDir, + stagingFolderPath: _stagingFolderPath, + stagingDir: _stagingFolderPath, + rooibos: { + isGlobalMethodMockingEnabled: true + } + }); + }); + + afterEach(() => { + destroyProgram(); }); describe('basic tests', () => { @@ -594,29 +601,15 @@ describe('RooibosPlugin', () => { it('adds launch hook with custom scene', async () => { - options = { + setupProgram({ rootDir: _rootDir, stagingFolderPath: _stagingFolderPath, stagingDir: _stagingFolderPath, rooibos: { testSceneName: 'CustomRooibosScene' } - }; - plugin = new RooibosPlugin(); - fsExtra.ensureDirSync(_stagingFolderPath); - fsExtra.ensureDirSync(_rootDir); - fsExtra.ensureDirSync(tmpPath); - - builder = new ProgramBuilder(); - builder.options = util.normalizeAndResolveConfig(options); - builder.program = new Program(builder.options); - program = builder.program; - program.plugins.add(plugin); - program.createSourceScope(); //ensure source scope is created - plugin.beforeProgramCreate(builder); - plugin.fileFactory['options'].frameworkSourcePath = path.resolve(path.join('../framework/src/source')); - plugin.afterProgramCreate(program); - // program.validate(); + }); + const file = program.setFile('source/main.bs', ` sub main() print "main" @@ -1908,34 +1901,14 @@ describe('RooibosPlugin', () => { `; beforeEach(() => { - plugin = new RooibosPlugin(); - options = { + setupProgram({ rootDir: _rootDir, stagingFolderPath: _stagingFolderPath - }; - fsExtra.ensureDirSync(_stagingFolderPath); - fsExtra.ensureDirSync(_rootDir); - fsExtra.ensureDirSync(tmpPath); - - builder = new ProgramBuilder(); - builder.options = util.normalizeAndResolveConfig(options); - builder.program = new Program(builder.options); - program = builder.program; - builder.program = new Program(builder.options); - program = builder.program; - program.plugins.add(plugin); - program.createSourceScope(); //ensure source scope is created - plugin.beforeProgramCreate(builder); - plugin.fileFactory['options'].frameworkSourcePath = path.resolve(path.join('../framework/src/source')); - plugin.afterProgramCreate(program); - // program.validate(); + }); }); afterEach(() => { - fsExtra.ensureDirSync(tmpPath); - fsExtra.emptyDirSync(tmpPath); - builder.dispose(); - program.dispose(); + destroyProgram(); }); it('tag one', async () => { @@ -2057,9 +2030,9 @@ describe('RooibosPlugin', () => { expect(findMethod('getIgnoredTestInfo').func.body.statements).to.be.empty; await builder.transpile(); - let testContents = getTestFunctionContents(); + expect( - testContents + getTestFunctionContents() ).to.eql(undent` item = { id: "item" @@ -2078,7 +2051,6 @@ describe('RooibosPlugin', () => { end if `); - let a = getContents('rooibos/RuntimeConfig.brs'); expect( getContents('rooibos/RuntimeConfig.brs') ).to.eql(undent` @@ -2091,7 +2063,9 @@ describe('RooibosPlugin', () => { end function instance.getRuntimeConfig = function() return { - "reporter": "" + "reporters": [ + rooibos_ConsoleTestReporter + ] "failFast": true "sendHomeOnFinish": true "logLevel": 0 @@ -2143,6 +2117,85 @@ describe('RooibosPlugin', () => { expect(findMethod('getAllTestSuitesNames').func.body.statements).to.be.empty; expect(findMethod('getIgnoredTestInfo').func.body.statements).to.be.empty; }); + + const sep = '\n '; + const params: [string[], string][] = [ + [[], 'rooibos_ConsoleTestReporter'], + [['CONSOLE'], 'rooibos_ConsoleTestReporter'], + [['MyCustomReporter'], 'MyCustomReporter'], + [['JUnit', 'MyCustomReporter'], `rooibos_JUnitTestReporter${sep}MyCustomReporter`] + ]; + it('adds custom test reporters', async () => { + for (const [reporters, expected] of params) { + setupProgram({ + rootDir: _rootDir, + stagingFolderPath: _stagingFolderPath, + stagingDir: _stagingFolderPath, + rooibos: { + reporters: reporters + } + }); + + program.validate(); + expect(program.getDiagnostics()).to.be.empty; + + await builder.transpile(); + + expect( + getContents('rooibos/RuntimeConfig.brs') + ).to.eql(undent` + function __rooibos_RuntimeConfig_builder() + instance = {} + instance.new = sub() + end sub + instance.getVersionText = function() + return "${version}" + end function + instance.getRuntimeConfig = function() + return { + "reporters": [ + ${expected} + ] + "failFast": true + "sendHomeOnFinish": true + "logLevel": 0 + "showOnlyFailures": true + "printTestTimes": true + "lineWidth": 60 + "printLcov": false + "port": "invalid" + "catchCrashes": true + "throwOnFailedAssertion": false + "keepAppOpen": true + "isRecordingCodeCoverage": false + } + end function + instance.getTestSuiteClassWithName = function(name) + if false + ? "noop" + end if + end function + instance.getAllTestSuitesNames = function() + return [] + end function + instance.getIgnoredTestInfo = function() + return { + "count": 0 + "items": [] + } + end function + return instance + end function + function rooibos_RuntimeConfig() + instance = __rooibos_RuntimeConfig_builder() + instance.new() + return instance + end function + `); + + destroyProgram(); + } + }); }); describe.skip('run a local project', () => { diff --git a/docs/index.md b/docs/index.md index 720df32..f158c89 100644 --- a/docs/index.md +++ b/docs/index.md @@ -172,7 +172,11 @@ Here is the information converted into a Markdown table: | 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 | +| reporter? @deprecated 1 | string | The built-in reporter to use. Defaults to empty. Possible values are `console` and `junit`. | +| reporters? 2 | string[] | An array of factory functions/classes which implement `rooibos.BaseTestReporter`. Built-in reporters include `console` and `junit`. Defaults to `["console"]`. | +**1** This parameter is deprecated, use `reporters` instead. When specified, the reporter will be appended to the list of `reporters`. +**2** Custom reporters are not currently supported on [node-based tests](#testing-nodes), because rooibos does not know which files it should include in the generated test components. This will be addressed in a future Rooibos version (see issue [#266](https://github.com/georgejecook/rooibos/issues/266)). ## Creating test suites diff --git a/framework/src/source/BaseTestReporter.bs b/framework/src/source/BaseTestReporter.bs index 2f11427..15db20e 100644 --- a/framework/src/source/BaseTestReporter.bs +++ b/framework/src/source/BaseTestReporter.bs @@ -1,4 +1,8 @@ namespace rooibos + interface ITestReporterOnEndEvent + stats as rooibos.Stats + end interface + class BaseTestReporter public testRunner = invalid @@ -11,15 +15,11 @@ namespace rooibos m.allStats = runner.stats end function - function reportResults(allStats as dynamic) - 'override me - end function - - function testLogInfo(text as string) + function onBegin(ev as dynamic) 'override me end function - function testLogError(text as string) + function onEnd(ev as rooibos.ITestReporterOnEndEvent) 'override me end function diff --git a/framework/src/source/ConsoleTestReporter.bs b/framework/src/source/ConsoleTestReporter.bs index d0e609f..d33abb0 100644 --- a/framework/src/source/ConsoleTestReporter.bs +++ b/framework/src/source/ConsoleTestReporter.bs @@ -13,8 +13,8 @@ namespace rooibos end if end function - override function reportResults(allStats) - m.allStats = allStats + override function onEnd(ev as rooibos.ITestReporterOnEndEvent) + m.allStats = ev.stats m.startReport() for each testSuite in m.testRunner.testSuites if not m.allStats.hasFailures or ((not m.config.showOnlyFailures) or testSuite.stats.failedCount > 0 or testSuite.stats.crashedCount > 0) @@ -186,14 +186,6 @@ namespace rooibos '++ printing '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - override function testLogInfo(text) - ? "INFO " ; text - end function - - override function testLogError(text) - ? "ERROR " ; text - end function - function printLine(depth = 0, text = "") ? " " ; text end function diff --git a/framework/src/source/JUnitTestReporter.bs b/framework/src/source/JUnitTestReporter.bs index d7320c3..4fe3346 100644 --- a/framework/src/source/JUnitTestReporter.bs +++ b/framework/src/source/JUnitTestReporter.bs @@ -1,11 +1,7 @@ namespace rooibos class JUnitTestReporter extends rooibos.BaseTestReporter - function new(testRunner as dynamic) - super(testRunner) - end function - - override function reportResults(allStats as dynamic) + override function onEnd(ev as rooibos.ITestReporterOnEndEvent) root = createObject("roXMLElement") root.SetName("testsuites") properties = root.addElement("properties") diff --git a/framework/src/source/TestRunner.bs b/framework/src/source/TestRunner.bs index 05f183a..e216b0d 100644 --- a/framework/src/source/TestRunner.bs +++ b/framework/src/source/TestRunner.bs @@ -11,7 +11,7 @@ namespace rooibos ' */ class TestRunner public testScene = invalid - public testReporter = invalid + public testReporters = [] public nodeContext = invalid public config = invalid public testSuites = [] @@ -33,11 +33,7 @@ namespace rooibos m.runtimeConfig = new rooibos.RuntimeConfig() m.config = m.runtimeConfig.getRuntimeConfig() - if m.config.reporter = "JUnitTestReporter" - m.testReporter = new Rooibos.JUnitTestReporter(m) - else - m.testReporter = new Rooibos.ConsoleTestReporter(m) - end if + m.testReporters = m.getTestReporters() end function ' /** @@ -49,6 +45,12 @@ namespace rooibos ' */ public function run() + for each reporter in m.testReporters + if rooibos.common.isFunction(reporter.onBegin) + reporter.onBegin({}) + end if + end for + rooibosTimer = createObject("roTimespan") rooibosTimer.mark() suiteNames = m.runtimeConfig.getAllTestSuitesNames() @@ -103,7 +105,11 @@ namespace rooibos m.stats.time = rooibosTimer.totalMilliseconds() - m.testReporter.reportResults(m.stats) + for each reporter in m.testReporters + if rooibos.common.isFunction(reporter.onEnd) + reporter.onEnd({ stats: m.stats }) + end if + end for rooibosResult = { stats: m.stats @@ -307,6 +313,19 @@ namespace rooibos ut.PostFromString("") end function + private function getTestReporters() + reporters = [] + for each factory in m.config.reporters + if rooibos.common.isFunction(factory) + reporters.push(factory(m)) + end if + end for + if reporters.isEmpty() + reporters.push(new rooibos.ConsoleTestReporter(m)) + end if + return reporters + end function + end class '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++