Skip to content

Commit

Permalink
test: data-driven scope testing (#546)
Browse files Browse the repository at this point in the history
Closes partially #540.

### Summary of Changes

* Data-driven creation of scope tests
* Clean up of creation of grammar and formatter tests

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot authored Aug 9, 2023
1 parent c3c2aef commit f814f16
Show file tree
Hide file tree
Showing 20 changed files with 621 additions and 124 deletions.
2 changes: 1 addition & 1 deletion DSL/src/language-server/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
* - Services specified in this file
*
* @param context Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
* @return An object wrapping the shared services and the language-specific services
*/
export const createSafeDsServices = function (context: DefaultSharedModuleContext): {
shared: LangiumSharedServices;
Expand Down
59 changes: 47 additions & 12 deletions DSL/tests/formatting/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ export const createFormatterTests = async (): Promise<FormatterTest[]> => {

// Must contain exactly one separator
if (parts.length !== 2) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
originalCode: '',
expectedFormattedCode: '',
error: new SeparatorError(parts.length - 1),
};
return invalidTest(pathRelativeToResources, new SeparatorError(parts.length - 1));
}

// Original code must not contain syntax errors
Expand All @@ -35,12 +30,7 @@ export const createFormatterTests = async (): Promise<FormatterTest[]> => {
);

if (syntaxErrors.length > 0) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
originalCode,
expectedFormattedCode,
error: new SyntaxErrorsInOriginalCodeError(syntaxErrors),
};
return invalidTest(pathRelativeToResources, new SyntaxErrorsInOriginalCodeError(syntaxErrors));
}

return {
Expand All @@ -53,23 +43,68 @@ export const createFormatterTests = async (): Promise<FormatterTest[]> => {
return Promise.all(testCases);
};

/**
* Report a test that has errors.
*
* @param pathRelativeToResources The path to the test file relative to the resources directory.
* @param error The error that occurred.
*/
const invalidTest = (pathRelativeToResources: string, error: Error): FormatterTest => {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
originalCode: '',
expectedFormattedCode: '',
error,
};
};

/**
* Normalizes line breaks to `\n`.
*
* @param code The code to normalize.
* @return The normalized code.
*/
const normalizeLineBreaks = (code: string): string => {
return code.replace(/\r\n?/gu, '\n');
};

/**
* A description of a formatter test.
*/
interface FormatterTest {
/**
* The name of the test.
*/
testName: string;

/**
* The original code before formatting.
*/
originalCode: string;

/**
* The expected formatted code.
*/
expectedFormattedCode: string;

/**
* An error that occurred while creating the test. If this is undefined, the test is valid.
*/
error?: Error;
}

/**
* The file contained no or more than one separator.
*/
class SeparatorError extends Error {
constructor(readonly number_of_separators: number) {
super(`Expected exactly one separator but found ${number_of_separators}.`);
}
}

/**
* The original code contained syntax errors.
*/
class SyntaxErrorsInOriginalCodeError extends Error {
constructor(readonly syntaxErrors: Diagnostic[]) {
const syntaxErrorsAsString = syntaxErrors.map((e) => `- ${e.message}`).join(`\n`);
Expand Down
2 changes: 2 additions & 0 deletions DSL/tests/formatting/testFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const services = createSafeDsServices(EmptyFileSystem).SafeDs;
const formatterTests = createFormatterTests();

describe('formatter', async () => {
// Test that the original code is formatted correctly
it.each(await formatterTests)('$testName', async (test) => {
// Test is invalid
if (test.error) {
Expand All @@ -24,6 +25,7 @@ describe('formatter', async () => {
await clearDocuments(services);
});

// Test that the expected formatted code stays the same when formatted again
it.each(await formatterTests)('$testName (idempotence)', async (test) => {
// Test is invalid
if (test.error) {
Expand Down
59 changes: 37 additions & 22 deletions DSL/tests/grammar/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,24 @@ import { NoCommentsError } from '../helpers/testChecks';
export const createGrammarTests = (): GrammarTest[] => {
return listTestResources('grammar').map((pathRelativeToResources): GrammarTest => {
const absolutePath = resolvePathRelativeToResources(path.join('grammar', pathRelativeToResources));
const program = fs.readFileSync(absolutePath).toString();
const comments = findTestComments(program);
const code = fs.readFileSync(absolutePath).toString();
const comments = findTestComments(code);

// Must contain at least one comment
if (comments.length === 0) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
program,
expectedResult: 'invalid',
error: new NoCommentsError(),
};
return invalidTest(pathRelativeToResources, new NoCommentsError());
}

// Must contain no more than one comment
if (comments.length > 1) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
program,
expectedResult: 'invalid',
error: new MultipleCommentsError(comments),
};
return invalidTest(pathRelativeToResources, new MultipleCommentsError(comments));
}

const comment = comments[0];

// Must contain a valid comment
if (comment !== 'syntax_error' && comment !== 'no_syntax_error') {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
program,
expectedResult: 'invalid',
error: new InvalidCommentError(comment),
};
return invalidTest(pathRelativeToResources, new InvalidCommentError(comment));
}

let testName: string;
Expand All @@ -51,19 +36,49 @@ export const createGrammarTests = (): GrammarTest[] => {

return {
testName,
program,
code,
expectedResult: comment,
};
});
};

/**
* Report a test that has errors.
*
* @param pathRelativeToResources The path to the test file relative to the resources directory.
* @param error The error that occurred.
*/
const invalidTest = (pathRelativeToResources: string, error: Error): GrammarTest => {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
code: '',
expectedResult: 'invalid',
error,
};
};

/**
* A description of a grammar test.
*/
interface GrammarTest {
/**
* The name of the test.
*/
testName: string;
program: string;

/**
* The code to parse.
*/
code: string;

/**
* The expected result after parsing the program.
*/
expectedResult: 'syntax_error' | 'no_syntax_error' | 'invalid';

/**
* An error that occurred while creating the test. If this is undefined, the test is valid.
*/
error?: Error;
}

Expand Down
2 changes: 1 addition & 1 deletion DSL/tests/grammar/testGrammar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('grammar', () => {
}

// Get the actual syntax errors
const { diagnostics } = await validationHelper(services)(test.program);
const { diagnostics } = await validationHelper(services)(test.code);
const syntaxErrors = diagnostics.filter(
(d) => d.severity === 1 && (d.code === 'lexing-error' || d.code === 'parsing-error'),
);
Expand Down
96 changes: 96 additions & 0 deletions DSL/tests/helpers/location.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, it, expect } from 'vitest';
import { isLocationEqual, locationToString, positionToString, rangeToString } from './location';

describe('positionToString', () => {
it.each([
{
position: { line: 0, character: 0 },
expected: '1:1',
},
{
position: { line: 1, character: 0 },
expected: '2:1',
},
])('should convert position to string ($expected)', ({ position, expected }) => {
expect(positionToString(position)).toBe(expected);
});
});

describe('rangeToString', () => {
it.each([
{
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
expected: '1:1 -> 1:1',
},
{
range: { start: { line: 0, character: 0 }, end: { line: 1, character: 0 } },
expected: '1:1 -> 2:1',
},
])('should convert range to string ($expected)', ({ range, expected }) => {
expect(rangeToString(range)).toBe(expected);
});
});

describe('locationToString', () => {
it.each([
{
location: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
},
expected: 'file:///test.sdstest:1:1 -> 1:1',
},
{
location: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 1, character: 0 } },
},
expected: 'file:///test.sdstest:1:1 -> 2:1',
},
])(`should convert location to string ($expected)`, ({ location, expected }) => {
expect(locationToString(location)).toBe(expected);
});
});

describe('isLocationEqual', () => {
it.each([
{
location1: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
},
location2: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
},
expected: true,
id: 'same location',
},
{
location1: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
},
location2: {
uri: 'file:///test2.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
},
expected: false,
id: 'different uri',
},
{
location1: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
},
location2: {
uri: 'file:///test.sdstest',
range: { start: { line: 0, character: 0 }, end: { line: 1, character: 0 } },
},
expected: false,
id: 'different range',
},
])('should compare locations for equality ($id)', ({ location1, location2, expected }) => {
expect(isLocationEqual(location1, location2)).toBe(expected);
});
});
43 changes: 43 additions & 0 deletions DSL/tests/helpers/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Location, Position, Range } from 'vscode-languageserver';
import { isRangeEqual } from 'langium/test';

/**
* Converts a position to a string.
*
* @param position The position to convert.
* @returns The string representation of the position.
*/
export const positionToString = (position: Position): string => {
return `${position.line + 1}:${position.character + 1}`;
};

/**
* Converts a range to a string.
*
* @param range The range to convert.
* @returns The string representation of the range.
*/
export const rangeToString = (range: Range): string => {
return `${positionToString(range.start)} -> ${positionToString(range.end)}`;
};

/**
* Converts a location to a string.
*
* @param location The location to convert.
* @returns The string representation of the location.
*/
export const locationToString = (location: Location) => {
return `${location.uri}:${rangeToString(location.range)}`;
};

/**
* Compare two locations for equality.ts.
*
* @param location1 The first location.
* @param location2 The second location.
* @returns True if the locations are equal, false otherwise.
*/
export const isLocationEqual = (location1: Location, location2: Location): boolean => {
return location1.uri === location2.uri && isRangeEqual(location1.range, location2.range);
};
Loading

0 comments on commit f814f16

Please sign in to comment.