• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / rooibos / 28900108396

07 Jul 2026 09:31PM UTC coverage: 83.256%. First build
28900108396

Pull #408

github

web-flow
Merge e4cdb7d73 into 19e4a7083
Pull Request #408: Fix duplicate node-test file instances corrupting build output

695 of 918 branches covered (75.71%)

Branch coverage included in aggregate %.

15 of 17 new or added lines in 2 files covered. (88.24%)

1110 of 1250 relevant lines covered (88.8%)

237.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.27
/src/plugin.ts
1
import type {
2
    BscFile,
3
    CompilerPlugin,
4
    ProgramBuilder,
5
    XmlFile,
6
    OnPrepareFileEvent,
7
    BeforeBuildProgramEvent,
8
    AfterProvideFileEvent,
9
    BeforeProvideProgramEvent,
10
    AfterRemoveFileEvent,
11
    AfterProvideProgramEvent,
12
    AfterValidateProgramEvent
13
} from 'brighterscript';
14
import {
1✔
15
    isBrsFile,
16
    isXmlFile,
17
    util,
18
    standardizePath
19
} from 'brighterscript';
20
import { RooibosSession } from './lib/rooibos/RooibosSession';
1✔
21
import { CodeCoverageProcessor } from './lib/rooibos/CodeCoverageProcessor';
1✔
22
import { FileFactory } from './lib/rooibos/FileFactory';
1✔
23
import type { RooibosConfig } from './lib/rooibos/RooibosConfig';
24
import * as minimatch from 'minimatch';
1✔
25
import * as path from 'path';
1✔
26
import { MockUtil } from './lib/rooibos/MockUtil';
1✔
27
import { getScopeForSuite } from './lib/rooibos/Utils';
1✔
28
import { RooibosLogPrefix } from './lib/utils/Diagnostics';
1✔
29

30
export class RooibosPlugin implements CompilerPlugin {
1✔
31

32
    public name = 'rooibosPlugin';
97✔
33
    public session: RooibosSession;
34
    public codeCoverageProcessor: CodeCoverageProcessor;
35
    public mockUtil: MockUtil;
36
    public fileFactory: FileFactory;
37
    public _builder: ProgramBuilder;
38
    public config: RooibosConfig;
39

40
    beforeProvideProgram(event: BeforeProvideProgramEvent): void {
41
        const builder = event.builder;
97✔
42
        this._builder = builder;
97✔
43

44
        this.config = this.getConfig((builder.options as any).rooibos || {});
97✔
45

46
        this.fileFactory = new FileFactory();
97✔
47
        if (!this.session) {
97!
48
            this.session = new RooibosSession(builder, this.fileFactory);
97✔
49
            this.codeCoverageProcessor = new CodeCoverageProcessor(builder, this.fileFactory);
97✔
50
            this.mockUtil = new MockUtil(builder, this.session);
97✔
51
        }
52
    }
53
    private getConfig(options: any) {
54
        let config: RooibosConfig = options;
97✔
55
        if (config.printTestTimes === undefined) {
97!
56
            config.printTestTimes = true;
97✔
57
        }
58
        if (config.catchCrashes === undefined) {
97!
59
            config.catchCrashes = true;
97✔
60
        }
61
        if (config.colorizeOutput === undefined) {
97!
62
            config.colorizeOutput = false;
97✔
63
        }
64
        if (config.throwOnFailedAssertion === undefined) {
97!
65
            config.throwOnFailedAssertion = false;
97✔
66
        }
67
        if (config.sendHomeOnFinish === undefined) {
97!
68
            config.sendHomeOnFinish = true;
97✔
69
        }
70
        if (config.failFast === undefined) {
97!
71
            config.failFast = true;
97✔
72
        }
73
        if (config.showOnlyFailures === undefined) {
97!
74
            config.showOnlyFailures = true;
97✔
75
        }
76
        if (config.isRecordingCodeCoverage === undefined) {
97✔
77
            config.isRecordingCodeCoverage = false;
88✔
78
        }
79
        if (config.isGlobalMethodMockingEnabled === undefined) {
97✔
80
            config.isGlobalMethodMockingEnabled = false;
20✔
81
        }
82
        if (config.isGlobalMethodMockingEfficientMode === undefined) {
97✔
83
            config.isGlobalMethodMockingEfficientMode = true;
87✔
84
        }
85
        if (config.keepAppOpen === undefined) {
97!
86
            config.keepAppOpen = true;
97✔
87
        }
88
        if (config.testSceneName === undefined) {
97✔
89
            config.testSceneName = 'RooibosScene';
96✔
90
        }
91
        //ignore roku modules by default
92
        if (config.includeFilters === undefined) {
97!
93
            config.includeFilters = [
97✔
94
                '**/*.spec.bs',
95
                '!**/BaseTestSuite.spec.bs',
96
                '!**/roku_modules/**/*'];
97
        }
98

99
        const defaultCoverageExcluded = [
97✔
100
            '**/*.spec.bs',
101
            '**/roku_modules/**/*',
102
            '**/source/main.bs',
103
            '**/source/rooibos/**/*',
104
            '**/components/rooibos/**/*'
105
        ];
106

107
        // Set default coverage exclusions, or merge with defaults if available.
108
        if (config.coverageExcludedFiles === undefined) {
97✔
109
            config.coverageExcludedFiles = defaultCoverageExcluded;
88✔
110
        } else {
111
            config.coverageExcludedFiles.push(...defaultCoverageExcluded);
9✔
112
        }
113

114
        const defaultGlobalMethodMockingExcluded = [
97✔
115
            '**/*.spec.bs',
116
            '**/source/main.bs',
117
            '**/source/rooibos/**/*',
118
            '**/components/rooibos/**/*'
119
        ];
120
        if (config.globalMethodMockingExcludedFiles === undefined) {
97✔
121
            config.globalMethodMockingExcludedFiles = defaultGlobalMethodMockingExcluded;
87✔
122
        }
123

124
        return config;
97✔
125
    }
126

127
    afterProvideProgram(event: AfterProvideProgramEvent) {
128
        this.fileFactory.addFrameworkFiles(event.program);
121✔
129
    }
130

131
    afterRemoveFile(event: AfterRemoveFileEvent) {
132
        // eslint-disable-next-line @typescript-eslint/dot-notation
133
        const xmlFile = event.file['rooibosXmlFile'] as XmlFile;
624✔
134
        if (xmlFile) {
624!
135
            // Remove the old generated xml files
136
            this._builder.program.removeFile(xmlFile.srcPath);
×
137
        }
138
    }
139

140
    afterProvideFile(event: AfterProvideFileEvent): void {
141
        for (const file of event.files) {
3,246✔
142

143
            if (!(isBrsFile(file) || isXmlFile(file)) || this.shouldSkipFile(file)) {
3,251!
144
                continue;
×
145
            }
146
            if (util.pathToUri(file.srcPath).includes('/rooibos/bsc-plugin/dist/framework')) {
3,251!
147
                // eslint-disable-next-line @typescript-eslint/dot-notation
148
                return;
×
149
            }
150
            if (this.fileFactory.isIgnoredFile(file) || !this.shouldSearchInFileForTests(file)) {
3,251✔
151
                return;
3,188✔
152
            }
153
            event.program.logger.log(RooibosLogPrefix, 'Processing test file', file.pkgPath);
63✔
154

155
            if (isBrsFile(file)) {
63!
156
                // Add the node test component so brighter script can validate the test files
157
                let suites = this.session.processFile(file);
63✔
158
                let nodeSuites = suites.filter((ts) => ts.isNodeTest);
70✔
159
                for (const suite of nodeSuites) {
63✔
160
                    const xmlFile = this._builder.program.setFile({
5✔
161
                        src: path.resolve(suite.xmlPkgPath),
162
                        dest: suite.xmlPkgPath
163
                    }, this.session.getNodeTestXmlText(suite));
164
                    // eslint-disable-next-line @typescript-eslint/dot-notation
165
                    file['rooibosXmlFile'] = xmlFile;
5✔
166
                    event.files.push(xmlFile);
5✔
167
                }
168
            }
169
        }
170
    }
171

172
    beforeBuildProgram(event: BeforeBuildProgramEvent) {
173
        const createdFiles = this.session.prepareForTranspile(event.editor, event.program, this.mockUtil);
68✔
174
        for (const file of createdFiles) {
68✔
175
            //if the build already includes a (now stale) file instance for this path, replace it rather than
176
            //adding a duplicate. Two instances for the same path would both be serialized, racing to write the
177
            //same output file (which intermittently produces corrupt output)
178
            const existingIndex = event.files.findIndex(x => x.destPath === file.destPath);
62✔
179
            if (existingIndex >= 0) {
2!
NEW
180
                event.files[existingIndex] = file;
×
181
            } else {
182
                event.files.push(file);
2✔
183
            }
184
        }
185
    }
186

187
    prepareFile(event: OnPrepareFileEvent) {
188
        if (this.shouldSkipFile(event.file)) {
1,912✔
189
            return;
68✔
190
        }
191
        let testSuite = this.session.sessionInfo.testSuitesToRun.find((ts) => ts.file.pkgPath === event.file.pkgPath);
1,844✔
192
        if (testSuite) {
1,844✔
193
            const scope = getScopeForSuite(testSuite);
40✔
194
            let noEarlyExit = testSuite.annotation.noEarlyExit;
40✔
195
            if (noEarlyExit) {
40!
196
                event.program.logger.warn(RooibosLogPrefix, `TestSuite "${testSuite.name}" is marked as noEarlyExit`);
×
197
            }
198

199
            const modifiedTestCases = new Set();
40✔
200
            testSuite.addDataFunctions(event.editor as any);
40✔
201
            for (let group of [...testSuite.testGroups.values()].filter((tg) => tg.isIncluded)) {
40✔
202
                for (let testCase of [...group.testCases].filter((tc) => tc.isIncluded)) {
46✔
203
                    let caseName = group.testSuite.generatedNodeName + group.file.pkgPath + testCase.funcName;
46✔
204
                    if (!modifiedTestCases.has(caseName)) {
46✔
205
                        group.modifyAssertions(testCase, noEarlyExit, event.editor as any, this.session.namespaceLookup, scope);
42✔
206
                        modifiedTestCases.add(caseName);
42✔
207
                    }
208

209
                }
210
            }
211
        }
212

213
        if (isBrsFile(event.file)) {
1,844✔
214
            if (this.shouldAddCodeCoverageToFile(event.file)) {
1,568✔
215
                this.codeCoverageProcessor.addCodeCoverage(event.file, event.editor);
8✔
216
            }
217
            if (this.shouldEnableGlobalMocksOnFile(event.file)) {
1,568✔
218
                this.mockUtil.enableGlobalMethodMocks(event.file, event.editor);
109✔
219
            }
220
        }
221
    }
222

223
    afterBuildProgram(event: BeforeBuildProgramEvent) {
224
        this.session.addLaunchHookFileIfNotPresent();
68✔
225
        this.codeCoverageProcessor.generateMetadata(this.config.isRecordingCodeCoverage, event.program);
68✔
226
    }
227

228
    afterValidateProgram(event: AfterValidateProgramEvent) {
229
        this.session.updateSessionStats();
87✔
230
        for (let testSuite of [...this.session.sessionInfo.testSuites.values()]) {
87✔
231
            testSuite.validate();
61✔
232
        }
233
        for (let file of this.fileFactory.addedFrameworkFiles) {
87✔
234
            // eslint-disable-next-line @typescript-eslint/dot-notation
235
            // file['diagnostics'] = [];
236
            event.program.diagnostics.clearForFile(file.srcPath);
2,262✔
237
        }
238
    }
239

240
    shouldSearchInFileForTests(file: BscFile) {
241
        if (!this.config.includeFilters || this.config.includeFilters.length === 0) {
3,251!
242
            return true;
×
243
        } else {
244
            for (let filter of this.config.includeFilters) {
3,251✔
245
                if (!minimatch(file.srcPath, filter, { dot: true })) {
3,377✔
246
                    return false;
3,188✔
247
                }
248
            }
249
        }
250
        return true;
63✔
251
    }
252
    shouldAddCodeCoverageToFile(file: BscFile) {
253
        if (!isBrsFile(file) || !this.config.isRecordingCodeCoverage) {
1,568✔
254
            return false;
1,361✔
255
        } else if (!this.config.coverageExcludedFiles) {
207!
256
            return true;
×
257
        } else {
258
            for (let filter of this.config.coverageExcludedFiles) {
207✔
259
                if (minimatch(file.destPath, filter, { dot: true })) {
1,021✔
260
                    return false;
199✔
261
                }
262
            }
263
        }
264
        return true;
8✔
265
    }
266

267
    shouldEnableGlobalMocksOnFile(file: BscFile) {
268
        if (!isBrsFile(file) || !this.config.isGlobalMethodMockingEnabled) {
1,568✔
269
            return false;
456✔
270
        } else if (!this.config.globalMethodMockingExcludedFiles) {
1,112!
271
            return true;
×
272
        } else {
273
            for (let filter of this.config.globalMethodMockingExcludedFiles) {
1,112✔
274
                if (minimatch(file.destPath, filter, { dot: true })) {
3,234✔
275
                    // console.log('±±±skipping file', file.pkgPath);
276
                    return false;
1,003✔
277
                }
278
            }
279
        }
280
        return true;
109✔
281
    }
282

283
    private shouldSkipFile(file: BscFile) {
284
        return file.pkgPath.toLowerCase().includes(standardizePath('source/bslib.brs'));
5,163✔
285
    }
286
}
287

288
export default () => {
1✔
289
    return new RooibosPlugin();
×
290
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc