This commit improves the validation logic in parser, corrects Windows collection files to adhere to expected structure. This validation helps catch errors that previously led to incomplete generated code in scripts for disabling VSCEIP and location settings. Changes: - Add type validation for function call structures in the parser/compiler. This helps prevent runtime errors by ensuring that only correctly structured data is processed. - Fix scripts in the Windows collection that previoulsy had incomplete `code` or `revertCode` values. These corrections ensure that the scripts function as intended. - Refactor related logic within the compiler/parser to improve testability and maintainability.
518 lines
21 KiB
TypeScript
518 lines
21 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type {
|
|
FunctionData, CodeInstruction,
|
|
ParameterDefinitionData, FunctionCallsData,
|
|
} from '@/application/collections/';
|
|
import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
|
|
import { parseSharedFunctions, type FunctionParameterFactory } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
|
|
import { createFunctionDataWithCall, createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
|
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
|
|
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
|
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
|
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
|
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
|
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
|
import type { FunctionCallsParser } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser';
|
|
import { createFunctionCallsParserStub } from '@tests/unit/shared/Stubs/FunctionCallsParserStub';
|
|
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
|
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
|
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
|
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
|
|
|
describe('SharedFunctionsParser', () => {
|
|
describe('parseSharedFunctions', () => {
|
|
describe('validates functions', () => {
|
|
it('throws when no name is provided', () => {
|
|
// arrange
|
|
const invalidFunctions = [
|
|
createFunctionDataWithCode()
|
|
.withCode('test function 1')
|
|
.withName(' '), // Whitespace,
|
|
createFunctionDataWithCode()
|
|
.withCode('test function 2')
|
|
.withName(undefined as unknown as string), // Undefined
|
|
createFunctionDataWithCode()
|
|
.withCode('test function 3')
|
|
.withName(''), // Empty
|
|
];
|
|
const expectedError = `Some function(s) have no names:\n${invalidFunctions.map((f) => JSON.stringify(f)).join('\n')}`;
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions(invalidFunctions)
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('throws when functions have duplicate names', () => {
|
|
// arrange
|
|
const name = 'same-func-name';
|
|
const expectedError = `duplicate function name: "${name}"`;
|
|
const functions = [
|
|
createFunctionDataWithCode().withName(name),
|
|
createFunctionDataWithCode().withName(name),
|
|
];
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions(functions)
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
describe('throws when functions have duplicate code', () => {
|
|
it('throws on code duplication', () => {
|
|
// arrange
|
|
const code = 'duplicate-code';
|
|
const expectedError = `duplicate "code" in functions: "${code}"`;
|
|
const functions = [
|
|
createFunctionDataWithoutCallOrCode().withName('func-1').withCode(code),
|
|
createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code),
|
|
];
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions(functions)
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('throws on revert code duplication', () => {
|
|
// arrange
|
|
const revertCode = 'duplicate-revert-code';
|
|
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
|
const functions = [
|
|
createFunctionDataWithoutCallOrCode()
|
|
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
|
createFunctionDataWithoutCallOrCode()
|
|
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
|
];
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions(functions)
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
});
|
|
describe('throws when both or neither code and call are defined', () => {
|
|
it('throws when both code and call are defined', () => {
|
|
// arrange
|
|
const functionName = 'invalid-function';
|
|
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
|
|
const invalidFunction = createFunctionDataWithoutCallOrCode()
|
|
.withName(functionName)
|
|
.withCode('code')
|
|
.withMockCall();
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions([invalidFunction])
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('throws when neither code nor call is defined', () => {
|
|
// arrange
|
|
const functionName = 'invalid-function';
|
|
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
|
const invalidFunction = createFunctionDataWithoutCallOrCode()
|
|
.withName(functionName);
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions([invalidFunction])
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
});
|
|
describe('throws when parameter types are invalid', () => {
|
|
const testScenarios: readonly {
|
|
readonly description: string;
|
|
readonly invalidType: unknown;
|
|
}[] = [
|
|
{
|
|
description: 'parameter is not an array',
|
|
invalidType: 5,
|
|
},
|
|
{
|
|
description: 'parameter array contains non-objects',
|
|
invalidType: ['a', { a: 'b' }],
|
|
},
|
|
];
|
|
for (const testCase of testScenarios) {
|
|
it(testCase.description, () => {
|
|
// arrange
|
|
const func = createFunctionDataWithCode()
|
|
.withParametersObject(testCase.invalidType as never);
|
|
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withFunctions([func])
|
|
.parseFunctions();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
}
|
|
});
|
|
it('validates function code as expected when code is defined', () => {
|
|
// arrange
|
|
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
|
|
const functionData = createFunctionDataWithCode()
|
|
.withCode('expected code to be validated')
|
|
.withRevertCode('expected revert code to be validated');
|
|
const validator = new CodeValidatorStub();
|
|
// act
|
|
new TestContext()
|
|
.withFunctions([functionData])
|
|
.withValidator(validator)
|
|
.parseFunctions();
|
|
// assert
|
|
validator.assertHistory({
|
|
validatedCodes: [functionData.code, functionData.revertCode],
|
|
rules: expectedRules,
|
|
});
|
|
});
|
|
describe('parameter creation', () => {
|
|
describe('rethrows including function name when creating parameter throws', () => {
|
|
// arrange
|
|
const invalidParameterName = 'invalid-function-parameter-name';
|
|
const functionName = 'functionName';
|
|
const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`;
|
|
const expectedInnerError = new Error('injected error');
|
|
const parameterFactory: FunctionParameterFactory = () => {
|
|
throw expectedInnerError;
|
|
};
|
|
const functionData = createFunctionDataWithCode()
|
|
.withName(functionName)
|
|
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
|
itThrowsContextualError({
|
|
// act
|
|
throwingAction: (wrapError) => {
|
|
new TestContext()
|
|
.withFunctions([functionData])
|
|
.withFunctionParameterFactory(parameterFactory)
|
|
.withErrorWrapper(wrapError)
|
|
.parseFunctions();
|
|
},
|
|
// assert
|
|
expectedWrappedError: expectedInnerError,
|
|
expectedContextMessage: expectedErrorMessage,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
describe('handles empty function data', () => {
|
|
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
|
|
// act
|
|
const actual = new TestContext()
|
|
.withFunctions(absentValue)
|
|
.parseFunctions();
|
|
// assert
|
|
expect(actual).to.not.equal(undefined);
|
|
}, { excludeUndefined: true, excludeNull: true });
|
|
});
|
|
describe('function with inline code', () => {
|
|
it('parses single function with code as expected', () => {
|
|
// arrange
|
|
const name = 'function-name';
|
|
const expected = createFunctionDataWithoutCallOrCode()
|
|
.withName(name)
|
|
.withCode('expected-code')
|
|
.withRevertCode('expected-revert-code')
|
|
.withParameters(
|
|
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
|
|
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
|
|
);
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions([expected])
|
|
.parseFunctions();
|
|
// expect
|
|
const actual = collection.getFunctionByName(name);
|
|
expectEqualName(expected, actual);
|
|
expectEqualParameters(expected.parameters, actual.parameters);
|
|
expectEqualFunctionWithInlineCode(expected, actual);
|
|
});
|
|
});
|
|
describe('function with calls', () => {
|
|
describe('parses single function correctly', () => {
|
|
it('parses name correctly', () => {
|
|
// arrange
|
|
const expectedName = 'expected-function-name';
|
|
const data = createFunctionDataWithCode()
|
|
.withName(expectedName);
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions([data])
|
|
.parseFunctions();
|
|
// expect
|
|
const actual = collection.getFunctionByName(expectedName);
|
|
expect(actual.name).to.equal(expectedName);
|
|
expectEqualName(data, actual);
|
|
});
|
|
it('parses parameters correctly', () => {
|
|
// arrange
|
|
const functionCallsParserStub = createFunctionCallsParserStub();
|
|
const expectedParameters: readonly ParameterDefinitionData[] = [
|
|
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
|
|
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
|
|
];
|
|
const data = createFunctionDataWithCode()
|
|
.withParameters(...expectedParameters);
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions([data])
|
|
.withFunctionCallsParser(functionCallsParserStub.parser)
|
|
.parseFunctions();
|
|
// expect
|
|
const actual = collection.getFunctionByName(data.name);
|
|
expectEqualParameters(expectedParameters, actual.parameters);
|
|
});
|
|
it('parses call correctly', () => {
|
|
// arrange
|
|
const functionCallsParserStub = createFunctionCallsParserStub();
|
|
const inputCallData = new FunctionCallDataStub()
|
|
.withName('function-input-call');
|
|
const data = createFunctionDataWithoutCallOrCode()
|
|
.withCall(inputCallData);
|
|
const expectedCall = new FunctionCallStub()
|
|
.withFunctionName('function-expected-call');
|
|
functionCallsParserStub.setup(inputCallData, [expectedCall]);
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions([data])
|
|
.withFunctionCallsParser(functionCallsParserStub.parser)
|
|
.parseFunctions();
|
|
// expect
|
|
const actualFunction = collection.getFunctionByName(data.name);
|
|
expectEqualFunctionWithCalls([expectedCall], actualFunction);
|
|
});
|
|
});
|
|
describe('parses multiple functions correctly', () => {
|
|
it('parses names correctly', () => {
|
|
// arrange
|
|
const expectedNames: readonly string[] = [
|
|
'expected-function-name-1',
|
|
'expected-function-name-2',
|
|
'expected-function-name-3',
|
|
];
|
|
const data: readonly FunctionData[] = expectedNames.map(
|
|
(functionName) => createFunctionDataWithCall()
|
|
.withName(functionName),
|
|
);
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions(data)
|
|
.parseFunctions();
|
|
// expect
|
|
expectedNames.forEach((name, index) => {
|
|
const compiledFunction = collection.getFunctionByName(name);
|
|
expectEqualName(data[index], compiledFunction);
|
|
});
|
|
});
|
|
it('parses parameters correctly', () => {
|
|
// arrange
|
|
const testData: readonly {
|
|
readonly functionName: string;
|
|
readonly inputParameterData: readonly ParameterDefinitionData[];
|
|
}[] = [
|
|
{
|
|
functionName: 'func1',
|
|
inputParameterData: [
|
|
new ParameterDefinitionDataStub().withName('func1-first-parameter'),
|
|
new ParameterDefinitionDataStub().withName('func1-second-parameter'),
|
|
],
|
|
},
|
|
{
|
|
functionName: 'func2',
|
|
inputParameterData: [
|
|
new ParameterDefinitionDataStub().withName('func2-optional-parameter').withOptionality(true),
|
|
new ParameterDefinitionDataStub().withName('func2-required-parameter').withOptionality(false),
|
|
],
|
|
},
|
|
];
|
|
const data: readonly FunctionData[] = testData.map(
|
|
(d) => createFunctionDataWithCall()
|
|
.withName(d.functionName)
|
|
.withParameters(...d.inputParameterData),
|
|
);
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions(data)
|
|
.parseFunctions();
|
|
// expect
|
|
testData.forEach(({ functionName, inputParameterData }) => {
|
|
const actualFunction = collection.getFunctionByName(functionName);
|
|
expectEqualParameters(inputParameterData, actualFunction.parameters);
|
|
});
|
|
});
|
|
it('parses call correctly', () => {
|
|
// arrange
|
|
const functionCallsParserStub = createFunctionCallsParserStub();
|
|
const callData: readonly {
|
|
readonly functionName: string;
|
|
readonly inputData: FunctionCallsData,
|
|
readonly expectedCalls: ReturnType<FunctionCallsParser>,
|
|
}[] = [
|
|
{
|
|
functionName: 'function-1',
|
|
inputData: new FunctionCallDataStub().withName('function-1-input-function-call'),
|
|
expectedCalls: [
|
|
new FunctionCallStub().withFunctionName('function-1-compiled-function-call'),
|
|
],
|
|
},
|
|
{
|
|
functionName: 'function-2',
|
|
inputData: [
|
|
new FunctionCallDataStub().withName('function-2-input-function-call-1'),
|
|
new FunctionCallDataStub().withName('function-2-input-function-call-2'),
|
|
],
|
|
expectedCalls: [
|
|
new FunctionCallStub().withFunctionName('function-2-compiled-function-call-1'),
|
|
new FunctionCallStub().withFunctionName('function-2-compiled-function-call-2'),
|
|
],
|
|
},
|
|
];
|
|
const data: readonly FunctionData[] = callData.map(
|
|
({ functionName, inputData }) => createFunctionDataWithoutCallOrCode()
|
|
.withName(functionName)
|
|
.withCall(inputData),
|
|
);
|
|
callData.forEach(({
|
|
inputData,
|
|
expectedCalls,
|
|
}) => functionCallsParserStub.setup(inputData, expectedCalls));
|
|
// act
|
|
const collection = new TestContext()
|
|
.withFunctions(data)
|
|
.withFunctionCallsParser(functionCallsParserStub.parser)
|
|
.parseFunctions();
|
|
// expect
|
|
callData.forEach(({ functionName, expectedCalls }) => {
|
|
const actualFunction = collection.getFunctionByName(functionName);
|
|
expectEqualFunctionWithCalls(expectedCalls, actualFunction);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
class TestContext {
|
|
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
|
|
|
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
|
|
|
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
|
|
|
|
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
|
|
|
private functionCallsParser: FunctionCallsParser = createFunctionCallsParserStub().parser;
|
|
|
|
private parameterFactory: FunctionParameterFactory = (
|
|
name: string,
|
|
isOptional: boolean,
|
|
) => new FunctionParameterStub()
|
|
.withName(name)
|
|
.withOptional(isOptional);
|
|
|
|
private parameterCollectionFactory
|
|
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
|
|
|
|
public withSyntax(syntax: ILanguageSyntax): this {
|
|
this.syntax = syntax;
|
|
return this;
|
|
}
|
|
|
|
public withValidator(codeValidator: ICodeValidator): this {
|
|
this.codeValidator = codeValidator;
|
|
return this;
|
|
}
|
|
|
|
public withFunctionCallsParser(functionCallsParser: FunctionCallsParser): this {
|
|
this.functionCallsParser = functionCallsParser;
|
|
return this;
|
|
}
|
|
|
|
public withFunctions(functions: readonly FunctionData[]): this {
|
|
this.functions = functions;
|
|
return this;
|
|
}
|
|
|
|
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
|
this.wrapError = wrapError;
|
|
return this;
|
|
}
|
|
|
|
public withFunctionParameterFactory(parameterFactory: FunctionParameterFactory): this {
|
|
this.parameterFactory = parameterFactory;
|
|
return this;
|
|
}
|
|
|
|
public withParameterCollectionFactory(
|
|
parameterCollectionFactory: FunctionParameterCollectionFactory,
|
|
): this {
|
|
this.parameterCollectionFactory = parameterCollectionFactory;
|
|
return this;
|
|
}
|
|
|
|
public parseFunctions(): ReturnType<typeof parseSharedFunctions> {
|
|
return parseSharedFunctions(
|
|
this.functions,
|
|
this.syntax,
|
|
{
|
|
codeValidator: this.codeValidator,
|
|
wrapError: this.wrapError,
|
|
createParameter: this.parameterFactory,
|
|
createParameterCollection: this.parameterCollectionFactory,
|
|
parseFunctionCalls: this.functionCallsParser,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
function expectEqualName(expected: FunctionData, actual: ISharedFunction): void {
|
|
expect(actual.name).to.equal(expected.name);
|
|
}
|
|
|
|
function expectEqualParameters(
|
|
expected: readonly ParameterDefinitionData[] | undefined,
|
|
actual: IReadOnlyFunctionParameterCollection,
|
|
): void {
|
|
const actualSimplifiedParameters = actual.all.map((parameter) => ({
|
|
name: parameter.name,
|
|
optional: parameter.isOptional,
|
|
}));
|
|
const expectedSimplifiedParameters = expected?.map((parameter) => ({
|
|
name: parameter.name,
|
|
optional: parameter.optional || false,
|
|
})) || [];
|
|
expect(expectedSimplifiedParameters).to.deep.equal(actualSimplifiedParameters, 'Unequal parameters');
|
|
}
|
|
|
|
function expectEqualFunctionWithInlineCode(
|
|
expected: CodeInstruction,
|
|
actual: ISharedFunction,
|
|
): void {
|
|
expectCodeFunctionBody(actual.body);
|
|
expect(actual.body.code, `function "${actual.name}" has no code`);
|
|
expect(actual.body.code.execute).to.equal(expected.code);
|
|
expect(actual.body.code.revert).to.equal(expected.revertCode);
|
|
}
|
|
|
|
function expectEqualFunctionWithCalls(
|
|
expectedCalls: readonly FunctionCall[],
|
|
actualFunction: ISharedFunction,
|
|
): void {
|
|
expectCallsFunctionBody(actualFunction.body);
|
|
const actualCalls = actualFunction.body.calls;
|
|
expect(actualCalls.length).to.equal(expectedCalls.length);
|
|
expect(actualCalls).to.have.members(expectedCalls);
|
|
}
|