Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/runtime global function mocking #250

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0e07019
Added function to bast test suite to store global mocked functions
chrisdp Jan 11, 2024
77d06a4
Moved some logic to the utils
chrisdp Jan 11, 2024
9f75924
Added logic to inject code that adds global mock checks at the start …
chrisdp Jan 11, 2024
90edc04
Fixed a reversed check
chrisdp Jan 15, 2024
2c4feef
Removed new api in favor of expanding stubcall and updated injection …
chrisdp Jan 15, 2024
9f6111e
Updated some of the global function detection logic
chrisdp Jan 15, 2024
fadbf2c
Unit tests, fixes, sample test project update
chrisdp Jan 15, 2024
fc6ce1d
removed console logs
chrisdp Jan 15, 2024
8b3fee5
Transpile the file if we had to touch modify it
chrisdp Jan 15, 2024
3307955
Fixed global mocks and stubs not clearing
chrisdp Jan 15, 2024
a155a29
updated some of the checks around transforming stubcall functions
chrisdp Jan 16, 2024
a342165
Made some code reusable and fixed some imports
chrisdp Jan 16, 2024
02c6f4e
Added back the disablemocking logic and removed unused code
chrisdp Jan 16, 2024
02c64df
Fixed bad ast related to noEarlyExit
chrisdp Jan 16, 2024
86656dd
Updated bsc in tests app
chrisdp Jan 16, 2024
db71048
added tests for global stubcall on device
chrisdp Jan 16, 2024
4c3d753
Fixed some device tests
chrisdp Jan 16, 2024
b4024ee
Fixed more on device tests
chrisdp Jan 16, 2024
67ec05a
More test fixes as the result of moving to ast editor
chrisdp Jan 16, 2024
bcfd24d
Fixed some indenting
chrisdp Jan 16, 2024
9f4eabd
Fixed node test xml files being added after modifying assertions lead…
chrisdp Jan 17, 2024
3acbf16
Updated the modify stub detection logic for globals
chrisdp Jan 17, 2024
1be78f6
more tests for global stub call modifications
chrisdp Jan 17, 2024
3164492
Fixed some race conditons and more global function detection refinments
chrisdp Jan 17, 2024
4132da2
Fixed some issues picking the wrong scope and make sure stubcall work…
chrisdp Jan 19, 2024
f435bba
Moved global stub clearing to clearStubs()
chrisdp Jan 20, 2024
a9e1450
Merge branch 'master' into feature/runtime-global-function-mocking
chrisdp Jan 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 89 additions & 44 deletions bsc-plugin/src/lib/rooibos/MockUtil.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,14 @@ describe('MockUtil', () => {
await builder.transpile();
let a = getContents('source/code.brs');
let b = trimLeading(`function sayHello(a1, a2)
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1,a2)
return result
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1, a2)
return __stubOrMockResult
else if type(__stubs_globalAa?.__globalStubs?.sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.sayhello
__stubOrMockResult = __stubFunction(a1, a2)
return __stubOrMockResult
end if
print "hello"
end function
Expand Down Expand Up @@ -109,9 +114,14 @@ describe('MockUtil', () => {
await builder.transpile();
let a = getContents('source/code.brs');
let b = trimLeading(`function sayHello(a1, a2)
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1,a2)
return result
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1, a2)
return __stubOrMockResult
else if type(__stubs_globalAa?.__globalStubs?.sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.sayhello
__stubOrMockResult = __stubFunction(a1, a2)
return __stubOrMockResult
end if
print "hello"
end function
Expand Down Expand Up @@ -143,8 +153,13 @@ describe('MockUtil', () => {
await builder.transpile();
let a = getContents('source/code.brs');
let b = trimLeading(`Sub RedLines_SetRulerLines(rulerLines)
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["redlines_setrulerlines"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["redlines_setrulerlines"].callback(rulerLines)
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["redlines_setrulerlines"].callback(rulerLines)
return
else if type(__stubs_globalAa?.__globalStubs?.redlines_setrulerlines).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.redlines_setrulerlines
__stubOrMockResult = __stubFunction(rulerLines)
return
end if
For Each line In rulerLines.Items()
Expand All @@ -153,9 +168,14 @@ describe('MockUtil', () => {
end Sub

Sub RedLines_AddLine(id, position, coords, node, childMap) as Object
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["redlines_addline"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["redlines_addline"].callback(id,position,coords,node,childMap)
return result
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["redlines_addline"].callback(id, position, coords, node, childMap)
return __stubOrMockResult
else if type(__stubs_globalAa?.__globalStubs?.redlines_addline).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.redlines_addline
__stubOrMockResult = __stubFunction(id, position, coords, node, childMap)
return __stubOrMockResult
end if
line = CreateObject("roSGNode", "Rectangle")
line.setField("id", id)
Expand Down Expand Up @@ -183,8 +203,13 @@ describe('MockUtil', () => {
await builder.transpile();
let a = getContents('source/code.brs');
let b = trimLeading(`sub sayHello(a1, a2)
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1,a2)
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback(a1, a2)
return
else if type(__stubs_globalAa?.__globalStubs?.sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.sayhello
__stubOrMockResult = __stubFunction(a1, a2)
return
end if
print "hello"
Expand Down Expand Up @@ -214,9 +239,14 @@ describe('MockUtil', () => {
await builder.transpile();
let a = getContents('source/code.brs');
let b = trimLeading(`function person_utils_sayHello(a1, a2)
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"].callback(a1,a2)
return result
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"].callback(a1, a2)
return __stubOrMockResult
else if type(__stubs_globalAa?.__globalStubs?.person_utils_sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.person_utils_sayhello
__stubOrMockResult = __stubFunction(a1, a2)
return __stubOrMockResult
end if
print "hello"
end function
Expand Down Expand Up @@ -245,8 +275,13 @@ describe('MockUtil', () => {
await builder.transpile();
let a = getContents('source/code.brs');
let b = trimLeading(`sub person_utils_sayHello(a1, a2)
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"] <> invalid
result = RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"].callback(a1,a2)
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["person_utils_sayhello"].callback(a1, a2)
return
else if type(__stubs_globalAa?.__globalStubs?.person_utils_sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.person_utils_sayhello
__stubOrMockResult = __stubFunction(a1, a2)
return
end if
print "hello"
Expand Down Expand Up @@ -313,42 +348,52 @@ describe('MockUtil', () => {
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
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 beings_sayHello()
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["beings_sayhello"] <> invalid
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["beings_sayhello"].callback()
return __stubOrMockResult
else if type(__stubs_globalAa?.__globalStubs?.beings_sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.beings_sayhello
__stubOrMockResult = __stubFunction()
return __stubOrMockResult
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 sayHello()
__stubs_globalAa = getGlobalAa()
if RBS_SM_1_getMocksByFunctionName()["sayhello"] <> invalid
__stubOrMockResult = RBS_SM_1_getMocksByFunctionName()["sayhello"].callback()
return __stubOrMockResult
else if type(__stubs_globalAa?.__globalStubs?.sayhello).endsWith("Function")
__stubFunction = __stubs_globalAa.__globalStubs.sayhello
__stubOrMockResult = __stubFunction()
return __stubOrMockResult
end if
print "hello3"
end function

function RBS_SM_1_getMocksByFunctionName()
if m._rMocksByFunctionName = invalid
m._rMocksByFunctionName = {}
end if
return m._rMocksByFunctionName
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);

Expand Down
94 changes: 86 additions & 8 deletions bsc-plugin/src/lib/rooibos/MockUtil.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* 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 type { BrsFile, Editor, NamespaceStatement, ProgramBuilder } from 'brighterscript';
import { ParseMode, Parser, Position, TokenKind, WalkMode, createVisitor, isClassStatement, isNamespaceStatement } from 'brighterscript';
import * as brighterscript from 'brighterscript';
import type { RooibosConfig } from './RooibosConfig';
import { RawCodeStatement } from './RawCodeStatement';
Expand All @@ -12,7 +12,7 @@ import type { RooibosSession } from './RooibosSession';
import { diagnosticErrorProcessingFile } from '../utils/Diagnostics';
import type { TestCase } from './TestCase';
import type { TestSuite } from './TestSuite';
import { getAllDottedGetParts } from './Utils';
import { functionRequiresReturnValue, getAllDottedGetParts } from './Utils';

export class MockUtil {

Expand Down Expand Up @@ -85,15 +85,26 @@ export class MockUtil {
for (let param of functionStatement.func.parameters) {
param.asToken = null;
}

const paramNames = functionStatement.func.parameters.map((param) => param.name.text).join(',');
const requiresReturnValue = functionRequiresReturnValue(functionStatement);
const globalAaName = '__stubs_globalAa';
const resultName = '__stubOrMockResult';
const storageName = '__globalStubs';

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`
const template = undent`
${globalAaName} = getGlobalAa()
if RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"] <> invalid
result = RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"].callback(${paramNames})
${returnStatement}
${resultName} = RBS_SM_${this.fileId}_getMocksByFunctionName()["${methodName}"].callback(${paramNames})
return${requiresReturnValue ? ` ${resultName}` : '' }
else if type(${globalAaName}?.${storageName}?.${methodName}).endsWith("Function")
__stubFunction = ${globalAaName}.${storageName}.${methodName}
${resultName} = __stubFunction(${paramNames})
return${requiresReturnValue ? ` ${resultName}` : ''}
end if
`));
`;
const astCodeToInject = Parser.parse(template).ast.statements;
this.astEditor.arrayUnshift(functionStatement.func.body.statements, ...astCodeToInject);

this.processedStatements.add(functionStatement);
}
Expand Down Expand Up @@ -158,6 +169,36 @@ export class MockUtil {
}
//modify args
let arg0 = callExpression.args[0];
let arg1 = callExpression.args[1];

if (isStubCall) {
if (brighterscript.isFunctionExpression(arg1)) {
if (brighterscript.isDottedGetExpression(arg0)) {
let nameParts = getAllDottedGetParts(arg0);
let name = nameParts.pop();
console.log(nameParts[0], namespaceLookup.has(nameParts[0].toLowerCase()));
chrisdp marked this conversation as resolved.
Show resolved Hide resolved

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.isVariableExpression(arg0)) {
const functionName = arg0.getName(ParseMode.BrightScript).toLowerCase();
this.session.globalStubbedMethods.add(functionName);
}
}
}

if (brighterscript.isCallExpression(arg0) && brighterscript.isDottedGetExpression(arg0.callee)) {

//is it a namespace?
Expand Down Expand Up @@ -186,4 +227,41 @@ export class MockUtil {
}
}

public addRuntimeGlobalFunctionMocks(file: BrsFile, astEditor: Editor) {
chrisdp marked this conversation as resolved.
Show resolved Hide resolved
file.ast.walk(createVisitor({
FunctionStatement: (statement, parent, owner, key) => {
if (!file.pkgPath.includes('rooibos/') && !file.pkgPath.endsWith('spec.brs')) {
chrisdp marked this conversation as resolved.
Show resolved Hide resolved
let isDisabledFoMocking = statement.annotations?.find(x => x.name.toLowerCase() === 'disablemocking');
chrisdp marked this conversation as resolved.
Show resolved Hide resolved
let parentNamespace = statement.findAncestor<NamespaceStatement>(isNamespaceStatement);
while (parentNamespace && !isDisabledFoMocking) {
if (parentNamespace) {
isDisabledFoMocking = parentNamespace.annotations?.find(x => x.name.toLowerCase() === 'disablemocking');
parentNamespace = parentNamespace.findAncestor<NamespaceStatement>(isNamespaceStatement);
}
}

if (!isDisabledFoMocking) {
const funcName = statement.getName(ParseMode.BrightScript);
const returnResult = functionRequiresReturnValue(statement);
const globalAaName = '__mocks_globalAa';
const storageName = '_globalMocks';
const template = undent`
${globalAaName} = getGlobalAa()
if type(${globalAaName}?.${storageName}?.${funcName}) = "roFunction" or type(${globalAaName}?.${storageName}?.${funcName}) = "Function" then
__mockedFunction = ${globalAaName}.${storageName}.${funcName}
__mockResult = __mockedFunction(${statement.func.parameters.map(x => x.name.text).join(', ')})
return ${returnResult ? '__mockResult' : ''}
end if
`;
const mockStatements = Parser.parse(template).ast.statements;
astEditor.arrayUnshift(statement.func.body.statements, ...mockStatements);
file.needsTranspiled = true;
}
}
}
}), {
walkMode: WalkMode.visitAllRecursive
});
}

}
14 changes: 11 additions & 3 deletions bsc-plugin/src/lib/rooibos/TestGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,22 @@ export class TestGroup extends TestBlock {
private modifyModernRooibosExpectCallExpression(callExpression: CallExpression, editor: AstEditor, namespaceLookup: Map<string, NamespaceContainer>) {
let isNotCalled = false;
let isStubCall = false;

//modify args
let arg0 = callExpression.args[0];
let arg1 = callExpression.args[1];
if (isDottedGetExpression(callExpression.callee)) {
const nameText = callExpression.callee.name.text;
editor.setProperty(callExpression.callee.name, 'text', `_${nameText}`);
isNotCalled = nameText === 'expectNotCalled';
isStubCall = nameText === 'stubCall';

if (isStubCall && (brighterscript.isDottedGetExpression(arg0) || brighterscript.isVariableExpression(arg0)) && brighterscript.isFunctionExpression(arg1)) {
console.log('modifyModernRooibosExpectCallExpression', callExpression.callee.name);
chrisdp marked this conversation as resolved.
Show resolved Hide resolved
return;
}
editor.setProperty(callExpression.callee.name, 'text', `_${nameText}`);
}
//modify args
let arg0 = callExpression.args[0];

if (brighterscript.isCallExpression(arg0) && isDottedGetExpression(arg0.callee)) {

//is it a namespace?
Expand Down
8 changes: 7 additions & 1 deletion bsc-plugin/src/lib/rooibos/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BrsFile, ClassStatement, Expression, FunctionStatement, AnnotationExpression, AstEditor } from 'brighterscript';
import { type BrsFile, type ClassStatement, type Expression, type FunctionStatement, type AnnotationExpression, type AstEditor, TokenKind } from 'brighterscript';
import * as brighterscript from 'brighterscript';
import { diagnosticCorruptTestProduced } from '../utils/Diagnostics';

Expand Down Expand Up @@ -36,6 +36,12 @@ export function sanitizeBsJsonString(text: string) {
return `"${text ? text.replace(/"/g, '\'') : ''}"`;
}

export function functionRequiresReturnValue(statement: FunctionStatement) {
const returnTypeToken = statement.func.returnTypeToken;
const functionType = statement.func.functionType;
return !((functionType?.kind === TokenKind.Sub && (returnTypeToken === undefined || returnTypeToken?.kind === TokenKind.Void)) || returnTypeToken?.kind === TokenKind.Void);
}

export function getAllDottedGetParts(dg: brighterscript.DottedGetExpression) {
let parts = [dg?.name?.text];
let nextPart = dg.obj;
Expand Down
Loading