Skip to content

Commit

Permalink
test: improve tests for formatter and grammar (#544)
Browse files Browse the repository at this point in the history
### Summary of Changes

* Add tests for the idempotence of formatter
* Clear documents between tests to avoid colliding URIs
* Move test creation code to a separate file, so it can be tested
* Bump dependencies
* Various bug fixes

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot authored Aug 8, 2023
1 parent 9662469 commit c3c2aef
Show file tree
Hide file tree
Showing 14 changed files with 846 additions and 639 deletions.
810 changes: 400 additions & 410 deletions DSL/package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions DSL/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Statically checked Data Science programs.",
"version": "0.1.0",
"engines": {
"vscode": "^1.79.0"
"vscode": "^1.81.0"
},
"categories": [
"Programming Languages"
Expand Down Expand Up @@ -57,7 +57,7 @@
},
"dependencies": {
"chalk": "^5.3.0",
"chevrotain": "^11.0.1",
"chevrotain": "^11.0.2",
"commander": "^11.0.0",
"glob": "^10.3.3",
"langium": "^1.2.1",
Expand All @@ -69,12 +69,12 @@
"vscode-uri": "^3.0.7"
},
"devDependencies": {
"@types/node": "^18.16.18",
"@types/vscode": "^1.79.1",
"@vitest/coverage-v8": "^0.32.4",
"@vitest/ui": "^0.32.4",
"@types/node": "^18.17.3",
"@types/vscode": "^1.81.0",
"@vitest/coverage-v8": "^0.34.1",
"@vitest/ui": "^0.34.1",
"langium-cli": "^1.2.1",
"typescript": "^5.1.6",
"vitest": "^0.32.4"
"vitest": "^0.34.1"
}
}
2 changes: 1 addition & 1 deletion DSL/src/language-server/formatting/safe-ds-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ export class SafeDSFormatter extends AbstractFormatter {
formatter.keyword('}').prepend(noSpace());
} else {
formatter.nodes(...columns).prepend(indent());
formatter.keywords(',').prepend(noSpace()).append(newLine());
formatter.keywords(',').prepend(noSpace());
formatter.keyword('}').prepend(newLine());
}
}
Expand Down
79 changes: 79 additions & 0 deletions DSL/tests/formatting/creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { listTestResources, resolvePathRelativeToResources } from '../helpers/testResources';
import path from 'path';
import fs from 'fs';
import { validationHelper } from 'langium/test';
import { Diagnostic } from 'vscode-languageserver-types';
import { createSafeDsServices } from '../../src/language-server/safe-ds-module';
import { EmptyFileSystem } from 'langium';

const services = createSafeDsServices(EmptyFileSystem).SafeDs;
const separator = '// -----------------------------------------------------------------------------';

export const createFormatterTests = async (): Promise<FormatterTest[]> => {
const testCases = listTestResources('formatting').map(async (pathRelativeToResources): Promise<FormatterTest> => {
const absolutePath = resolvePathRelativeToResources(path.join('formatting', pathRelativeToResources));
const program = fs.readFileSync(absolutePath).toString();
const parts = program.split(separator);

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

// Original code must not contain syntax errors
const originalCode = normalizeLineBreaks(parts[0]).trimEnd();
const expectedFormattedCode = normalizeLineBreaks(parts[1]).trim();

const validationResult = await validationHelper(services)(parts[0]);
const syntaxErrors = validationResult.diagnostics.filter(
(d) => d.severity === 1 && (d.code === 'lexing-error' || d.code === 'parsing-error'),
);

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

return {
testName: `${pathRelativeToResources} should be formatted correctly`,
originalCode,
expectedFormattedCode,
};
});

return Promise.all(testCases);
};

const normalizeLineBreaks = (code: string): string => {
return code.replace(/\r\n?/gu, '\n');
};

interface FormatterTest {
testName: string;
originalCode: string;
expectedFormattedCode: string;
error?: Error;
}

class SeparatorError extends Error {
constructor(readonly number_of_separators: number) {
super(`Expected exactly one separator but found ${number_of_separators}.`);
}
}

class SyntaxErrorsInOriginalCodeError extends Error {
constructor(readonly syntaxErrors: Diagnostic[]) {
const syntaxErrorsAsString = syntaxErrors.map((e) => `- ${e.message}`).join(`\n`);

super(`Original code has syntax errors:\n${syntaxErrorsAsString}`);
}
}
95 changes: 22 additions & 73 deletions DSL/tests/formatting/testFormatter.test.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,42 @@
import { createSafeDsServices } from '../../src/language-server/safe-ds-module';
import { expectFormatting, validationHelper } from 'langium/test';
import { clearDocuments, expectFormatting } from 'langium/test';
import { describe, it } from 'vitest';
import { EmptyFileSystem } from 'langium';
import { listTestResources, resolvePathRelativeToResources } from '../helpers/testResources';
import path from 'path';
import fs from 'fs';
import { Diagnostic } from 'vscode-languageserver-types';
import { createFormatterTests } from './creator';

const services = createSafeDsServices({ ...EmptyFileSystem }).SafeDs;
const separator = '// -----------------------------------------------------------------------------';
const services = createSafeDsServices(EmptyFileSystem).SafeDs;
const formatterTests = createFormatterTests();

describe('formatter', async () => {
it.each(await createFormatterTest())('$testName', async (test) => {
it.each(await formatterTests)('$testName', async (test) => {
// Test is invalid
if (test.error) {
throw test.error;
}

// Formatting original code must result in expected formatted code
await expectFormatting(services)({
before: test.originalCode,
after: test.expectedFormattedCode,
});
});
});

const createFormatterTest = async (): Promise<FormatterTest[]> => {
const testCases = listTestResources('formatting').map(async (pathRelativeToResources): Promise<FormatterTest> => {
const absolutePath = resolvePathRelativeToResources(path.join('formatting', pathRelativeToResources));
const program = fs.readFileSync(absolutePath).toString();
const parts = program.split(separator);
// Clear loaded documents to avoid colliding URIs (https://github.com/langium/langium/issues/1146)
await clearDocuments(services);
});

// Must contain exactly one separator
if (parts.length !== 2) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
originalCode: '',
expectedFormattedCode: '',
error: new SeparatorError(parts.length - 1),
};
it.each(await formatterTests)('$testName (idempotence)', async (test) => {
// Test is invalid
if (test.error) {
throw test.error;
}

// Original code must not contain syntax errors
const originalCode = normalizeLineBreaks(parts[0]).trimEnd();
const expectedFormattedCode = normalizeLineBreaks(parts[1]).trim();

const validationResult = await validationHelper(services)(parts[0]);
const syntaxErrors = validationResult.diagnostics.filter(
(d) => d.severity === 1 && (d.code === 'lexing-error' || d.code === 'parsing-error'),
);

if (syntaxErrors.length > 0) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
originalCode,
expectedFormattedCode,
error: new SyntaxErrorsInOriginalCodeError(syntaxErrors),
};
}
// Formatting must be idempotent
await expectFormatting(services)({
before: test.expectedFormattedCode,
after: test.expectedFormattedCode,
});

return {
testName: `${pathRelativeToResources} should be formatted correctly`,
originalCode,
expectedFormattedCode,
};
// Clear loaded documents to avoid colliding URIs (https://github.com/langium/langium/issues/1146)
await clearDocuments(services);
});

return Promise.all(testCases);
};

const normalizeLineBreaks = (code: string): string => {
return code.replace(/\r\n?/gu, '\n');
};

interface FormatterTest {
testName: string;
originalCode: string;
expectedFormattedCode: string;
error?: Error;
}

class SeparatorError extends Error {
constructor(readonly number_of_separators: number) {
super(`Expected exactly one separator but found ${number_of_separators}.`);
}
}

class SyntaxErrorsInOriginalCodeError extends Error {
constructor(readonly syntaxErrors: Diagnostic[]) {
const syntaxErrorsAsString = syntaxErrors.map((e) => `- ${e.message}`).join(`\n`);

super(`Original code has syntax errors:\n${syntaxErrorsAsString}`);
}
}
});
86 changes: 86 additions & 0 deletions DSL/tests/grammar/creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { listTestResources, resolvePathRelativeToResources } from '../helpers/testResources';
import path from 'path';
import fs from 'fs';
import { findTestComments } from '../helpers/testComments';
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);

// Must contain at least one comment
if (comments.length === 0) {
return {
testName: `INVALID TEST FILE [${pathRelativeToResources}]`,
program,
expectedResult: 'invalid',
error: 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),
};
}

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),
};
}

let testName: string;
if (comment === 'syntax_error') {
testName = `[${pathRelativeToResources}] should have syntax errors`;
} else {
testName = `[${pathRelativeToResources}] should not have syntax errors`;
}

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

/**
* A description of a grammar test.
*/
interface GrammarTest {
testName: string;
program: string;
expectedResult: 'syntax_error' | 'no_syntax_error' | 'invalid';
error?: Error;
}

/**
* Found multiple test comments.
*/
class MultipleCommentsError extends Error {
constructor(readonly comments: string[]) {
super(`Found multiple test comments (grammar tests expect only one): ${comments}`);
}
}

/**
* Found one test comment but it was invalid.
*/
class InvalidCommentError extends Error {
constructor(readonly comment: string) {
super(`Invalid test comment (valid values are 'syntax_error' and 'no_syntax_error'): ${comment}`);
}
}
Loading

0 comments on commit c3c2aef

Please sign in to comment.