From 2a3ef334567286e6b71921fc79086d19faf9855f Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 7 Oct 2023 18:17:23 +0200 Subject: [PATCH] refactor: use URIs instead of paths (strings) in tests (#610) ### Summary of Changes It's difficult to work with arbitrary paths, since they might be relative to some other directory or absolute. This led to long variable names to precisely express what a path meant. This PR instead refactors the test creators and some helpers to provide URI objects instead. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- tests/helpers/testChecks.test.ts | 13 +- tests/helpers/testChecks.ts | 10 +- tests/helpers/testResources.test.ts | 120 ++++++++++++++---- tests/helpers/testResources.ts | 82 ++++++++---- tests/language/formatting/creator.ts | 33 +++-- tests/language/generation/creator.ts | 102 +++++++-------- .../generation/testGeneration.test.ts | 13 +- tests/language/grammar/creator.ts | 31 ++--- tests/language/partialEvaluation/creator.ts | 57 +++------ .../testPartialEvaluation.test.ts | 5 +- tests/language/scoping/creator.ts | 62 +++------ tests/language/scoping/testScoping.test.ts | 4 +- tests/language/typing/creator.ts | 57 +++------ tests/language/typing/testTyping.test.ts | 5 +- tests/language/validation/creator.ts | 62 ++++----- .../validation/testValidation.test.ts | 9 +- .../nested/not a python file.txt} | 0 .../nested/python file.py} | 0 .../not a python file.txt} | 0 .../python file.py} | 0 .../nested}/not a safe-ds file.txt | 0 .../nested}/pipeline file.sdspipe | 0 .../nested}/skip pipeline file.sdspipe | 0 .../nested}/skip stub file.sdsstub | 0 .../nested/skip test file.sdstest | 0 .../nested/stub file.sdsstub | 0 .../nested/test file.sdstest | 0 .../not a safe-ds file.txt} | 0 .../pipeline file.sdspipe} | 0 .../skip pipeline file.sdspipe} | 0 .../listSafeDsFiles/skip stub file.sdsstub | 0 .../listSafeDsFiles/skip test file.sdstest | 0 .../helpers/listSafeDsFiles/stub file.sdsstub | 0 .../helpers/listSafeDsFiles/test file.sdstest | 0 34 files changed, 332 insertions(+), 333 deletions(-) rename tests/resources/helpers/{listTestResources/nested/not a safe-ds file.txt => listPythonFiles/nested/not a python file.txt} (100%) rename tests/resources/helpers/{listTestResources/nested/pipeline file.sdspipe => listPythonFiles/nested/python file.py} (100%) rename tests/resources/helpers/{listTestResources/nested/skip pipeline file.sdspipe => listPythonFiles/not a python file.txt} (100%) rename tests/resources/helpers/{listTestResources/nested/skip stub file.sdsstub => listPythonFiles/python file.py} (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles/nested}/not a safe-ds file.txt (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles/nested}/pipeline file.sdspipe (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles/nested}/skip pipeline file.sdspipe (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles/nested}/skip stub file.sdsstub (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles}/nested/skip test file.sdstest (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles}/nested/stub file.sdsstub (100%) rename tests/resources/helpers/{listTestResources => listSafeDsFiles}/nested/test file.sdstest (100%) rename tests/resources/helpers/{listTestResources/skip test file.sdstest => listSafeDsFiles/not a safe-ds file.txt} (100%) rename tests/resources/helpers/{listTestResources/stub file.sdsstub => listSafeDsFiles/pipeline file.sdspipe} (100%) rename tests/resources/helpers/{listTestResources/test file.sdstest => listSafeDsFiles/skip pipeline file.sdspipe} (100%) create mode 100644 tests/resources/helpers/listSafeDsFiles/skip stub file.sdsstub create mode 100644 tests/resources/helpers/listSafeDsFiles/skip test file.sdstest create mode 100644 tests/resources/helpers/listSafeDsFiles/stub file.sdsstub create mode 100644 tests/resources/helpers/listSafeDsFiles/test file.sdstest diff --git a/tests/helpers/testChecks.test.ts b/tests/helpers/testChecks.test.ts index 02a289a6c..03c74d029 100644 --- a/tests/helpers/testChecks.test.ts +++ b/tests/helpers/testChecks.test.ts @@ -8,6 +8,7 @@ import { NoCommentsError, } from './testChecks.js'; import { Range } from 'vscode-languageserver'; +import { URI } from 'langium'; const uri = 'file:///test.sdstest'; @@ -75,7 +76,7 @@ ${OPEN}${CLOSE} id: 'two comments, two ranges', }, ])('should associated comments and ranges ($id)', ({ program, expected }) => { - const result = findTestChecks(program, uri); + const result = findTestChecks(program, URI.parse(uri)); expect(result.isOk).toBeTruthy(); if (result.isOk) { @@ -89,7 +90,7 @@ ${OPEN}${CLOSE} // $TEST$ no_syntax_error ${OPEN}\n${CLOSE}${CLOSE} `, - uri, + URI.parse(uri), ); expect(result.isErr).toBeTruthy(); @@ -104,7 +105,7 @@ ${OPEN}${CLOSE} // $TEST$ no_syntax_error ${OPEN}\n${OPEN}${OPEN}${CLOSE} `, - uri, + URI.parse(uri), ); expect(result.isErr).toBeTruthy(); @@ -119,7 +120,7 @@ ${OPEN}${CLOSE} // $TEST$ no_syntax_error ${OPEN}\n${CLOSE}${OPEN}\n${CLOSE} `, - uri, + URI.parse(uri), ); expect(result.isErr).toBeTruthy(); @@ -129,7 +130,7 @@ ${OPEN}${CLOSE} }); it('should report if no test comments are found if corresponding check is enabled', () => { - const result = findTestChecks('', uri, { failIfNoComments: true }); + const result = findTestChecks('', URI.parse(uri), { failIfNoComments: true }); expect(result.isErr).toBeTruthy(); if (result.isErr) { @@ -142,7 +143,7 @@ ${OPEN}${CLOSE} ` // $TEST$ no_syntax_error `, - uri, + URI.parse(uri), { failIfFewerRangesThanComments: true }, ); expect(result.isErr).toBeTruthy(); diff --git a/tests/helpers/testChecks.ts b/tests/helpers/testChecks.ts index 9c9745546..7e5a8b76e 100644 --- a/tests/helpers/testChecks.ts +++ b/tests/helpers/testChecks.ts @@ -2,6 +2,7 @@ import { Location, Range } from 'vscode-languageserver'; import { findTestComments } from './testComments.js'; import { findTestRanges, FindTestRangesError } from './testRanges.js'; import { Result } from 'true-myth'; +import { URI } from 'langium'; /** * Finds all test checks, i.e. test comments and their corresponding test ranges. @@ -12,7 +13,7 @@ import { Result } from 'true-myth'; */ export const findTestChecks = ( program: string, - uri: string, + uri: URI, options: FindTestChecksOptions = {}, ): Result => { const { failIfNoComments = false, failIfFewerRangesThanComments = false } = options; @@ -41,7 +42,12 @@ export const findTestChecks = ( return Result.err(new FewerRangesThanCommentsError(comments, ranges)); } - return Result.ok(comments.map((comment, index) => ({ comment, location: { uri, range: ranges[index] } }))); + return Result.ok( + comments.map((comment, index) => ({ + comment, + location: { uri: uri.toString(), range: ranges[index] }, + })), + ); }; /** diff --git a/tests/helpers/testResources.test.ts b/tests/helpers/testResources.test.ts index b5090ce4c..2700055f0 100644 --- a/tests/helpers/testResources.test.ts +++ b/tests/helpers/testResources.test.ts @@ -1,9 +1,33 @@ import { describe, expect, it } from 'vitest'; -import { listSafeDSResources, listTestsResourcesGroupedByParentDirectory } from './testResources.js'; +import { + listPythonFiles, + listSafeDsFiles, + listSafeDsFilesGroupedByParentDirectory, + ResourceName, + resourceNameToUri, + ShortenedResourceName, + uriToShortenedResourceName, +} from './testResources.js'; +import { URI } from 'langium'; -describe('listTestResources', () => { - it('should yield all Safe-DS files in a directory that are not skipped', () => { - const result = listSafeDSResources('helpers/listTestResources'); +describe('uriToShortenedResourceName', () => { + it('should return the corresponding resource name if no root resource name is given', () => { + const resourceName = 'helpers/listSafeDsFiles'; + const actual = uriToShortenedResourceName(resourceNameToUri(resourceName)); + expect(normalizeResourceName(actual)).toBe(normalizeResourceName(resourceName)); + }); + + it('should return a shortened resource name if a root resource name is given', () => { + const resourceName = 'helpers/nested/listSafeDsFiles'; + const actual = uriToShortenedResourceName(resourceNameToUri(resourceName), 'helpers/nested'); + expect(actual).toBe('listSafeDsFiles'); + }); +}); + +describe('listSafeDsFiles', () => { + it('should return all Safe-DS files in a resource directory that are not skipped', () => { + const rootResourceName = 'helpers/listSafeDsFiles'; + const actual = listSafeDsFiles(rootResourceName); const expected = [ 'pipeline file.sdspipe', 'stub file.sdsstub', @@ -12,45 +36,85 @@ describe('listTestResources', () => { 'nested/stub file.sdsstub', 'nested/test file.sdstest', ]; - expect(normalizePaths(result)).toStrictEqual(normalizePaths(expected)); + + expectFileListsToMatch(rootResourceName, actual, expected); + }); +}); + +describe('listPythonFiles', () => { + it('should return all Python files in a resource directory', () => { + const rootResourceName = 'helpers/listPythonFiles'; + const actual = listPythonFiles(rootResourceName); + const expected = ['python file.py', 'nested/python file.py']; + + expectFileListsToMatch(rootResourceName, actual, expected); }); }); -describe('listTestResourcesGroupedByParentDirectory', () => { - it('should yield all Safe-DS files in a directory that are not skipped and group them by parent directory', () => { - const result = listTestsResourcesGroupedByParentDirectory('helpers/listTestResources'); +describe('listSafeDsFilesGroupedByParentDirectory', () => { + it('should return all Safe-DS files in a directory that are not skipped and group them by parent directory', () => { + const rootResourceName = 'helpers/listSafeDsFiles'; + const result = new Map(listSafeDsFilesGroupedByParentDirectory(rootResourceName)); - const keys = Object.keys(result); - expect(normalizePaths(keys)).toStrictEqual(normalizePaths(['.', 'nested'])); + // Compare the keys, i.e. the parent directories + const actualKeys = [...result.keys()]; + const expectedKeys = ['', 'nested']; + expectFileListsToMatch(rootResourceName, actualKeys, expectedKeys); - const directlyInRoot = result['.']; - expect(normalizePaths(directlyInRoot)).toStrictEqual( - normalizePaths(['pipeline file.sdspipe', 'stub file.sdsstub', 'test file.sdstest']), - ); + // Compare the values, i.e. the files, in the root directory + const actualValuesDirectlyInRoot = [...result.entries()].find( + ([key]) => uriToShortenedResourceName(key, rootResourceName) === '', + )!; + const expectedValuesDirectlyInRoot = ['pipeline file.sdspipe', 'stub file.sdsstub', 'test file.sdstest']; + expectFileListsToMatch(rootResourceName, actualValuesDirectlyInRoot[1], expectedValuesDirectlyInRoot); - const inNested = result.nested; - expect(normalizePaths(inNested)).toStrictEqual( - normalizePaths(['nested/pipeline file.sdspipe', 'nested/stub file.sdsstub', 'nested/test file.sdstest']), - ); + // Compare the values, i.e. the files, in the nested directory + const actualValuesInNested = [...result.entries()].find( + ([key]) => uriToShortenedResourceName(key, rootResourceName) === 'nested', + )!; + const expectedValuesInNested = [ + 'nested/pipeline file.sdspipe', + 'nested/stub file.sdsstub', + 'nested/test file.sdstest', + ]; + expectFileListsToMatch(rootResourceName, actualValuesInNested[1], expectedValuesInNested); }); }); /** - * Normalizes the given paths by replacing backslashes with slashes and sorting them. + * Asserts that the actual uris and the expected shortened resource names point to the same files. + * + * @param rootResourceName The root resource name. + * @param actualUris The actual URIs computed by some function under test. + * @param expectedShortenedResourceNames The expected shortened resource names. + */ +const expectFileListsToMatch = ( + rootResourceName: ResourceName, + actualUris: URI[], + expectedShortenedResourceNames: ShortenedResourceName[], +): void => { + const actualShortenedResourceNames = actualUris.map((uri) => uriToShortenedResourceName(uri, rootResourceName)); + expect(normalizeResourceNames(actualShortenedResourceNames)).toStrictEqual( + normalizeResourceNames(expectedShortenedResourceNames), + ); +}; + +/** + * Normalizes the given resource names by replacing backslashes with slashes and sorting them. * - * @param paths The paths to normalize. - * @return The normalized paths. + * @param resourceNames The resource names to normalize. + * @return The normalized resource names. */ -const normalizePaths = (paths: string[]): string[] => { - return paths.map(normalizePath).sort(); +const normalizeResourceNames = (resourceNames: string[]): string[] => { + return resourceNames.map(normalizeResourceName).sort(); }; /** - * Normalizes the given path by replacing backslashes with slashes. + * Normalizes the given resource name by replacing backslashes with slashes. * - * @param path The path to normalize. - * @return The normalized path. + * @param resourceName The resource name to normalize. + * @return The normalized resource name. */ -const normalizePath = (path: string): string => { - return path.replace(/\\/gu, '/'); +const normalizeResourceName = (resourceName: string): string => { + return resourceName.replace(/\\/gu, '/'); }; diff --git a/tests/helpers/testResources.ts b/tests/helpers/testResources.ts index 7d8c86c84..12381fe90 100644 --- a/tests/helpers/testResources.ts +++ b/tests/helpers/testResources.ts @@ -2,57 +2,87 @@ import path from 'path'; import { globSync } from 'glob'; import { SAFE_DS_FILE_EXTENSIONS } from '../../src/language/helpers/fileExtensions.js'; import { group } from 'radash'; +import { URI } from 'langium'; const resourcesPath = path.join(__dirname, '..', 'resources'); /** - * Resolves the given path relative to `tests/resources/`. + * A path relative to `tests/resources/`. + */ +export type ResourceName = string; + +/** + * A path relative to `tests/resources/` or a subdirectory thereof. + */ +export type ShortenedResourceName = string; + +/** + * Returns the URI that corresponds to the resource with the given name. * - * @param pathRelativeToResources The path relative to `tests/resources/`. - * @return The resolved absolute path. + * @param resourceName The resource name. + * @return The corresponding URI. */ -export const resolvePathRelativeToResources = (pathRelativeToResources: string) => { - return path.join(resourcesPath, pathRelativeToResources); +export const resourceNameToUri = (resourceName: ResourceName): URI => { + return URI.file(path.join(resourcesPath, resourceName)); }; /** - * Lists all Safe-DS files in the given directory relative to `tests/resources/` that are not skipped. + * Returns the resource name that corresponds to the given URI. If `rootResourceName` is given, the result is relative + * to `tests/resources/`. Otherwise, the result is relative to `tests/resources/`. * - * @param pathRelativeToResources The root directory relative to `tests/resources/`. - * @return Paths to the Safe-DS files relative to `pathRelativeToResources`. + * @param uri The URI. + * @param rootResourceName The corresponding root resource name. */ -export const listSafeDSResources = (pathRelativeToResources: string): string[] => { +export const uriToShortenedResourceName = (uri: URI, rootResourceName?: ResourceName): ShortenedResourceName => { + const rootPath = rootResourceName ? path.join(resourcesPath, rootResourceName) : resourcesPath; + return path.relative(rootPath, uri.fsPath); +}; + +/** + * Lists all Safe-DS files in the given root directory that are not skipped. + * + * @param rootResourceName The resource name of the root directory. + * @return URIs of the discovered Safe-DS files. + */ +export const listSafeDsFiles = (rootResourceName: ResourceName): URI[] => { const pattern = `**/*.{${SAFE_DS_FILE_EXTENSIONS.join(',')}}`; - const cwd = resolvePathRelativeToResources(pathRelativeToResources); + const cwd = resourceNameToUri(rootResourceName).fsPath; - return globSync(pattern, { cwd, nodir: true }).filter(isNotSkipped); + return globSync(pattern, { cwd, nodir: true }) + .filter(isNotSkipped) + .map((it) => URI.file(path.join(cwd, it))); }; /** - * Lists all Python files in the given directory relative to `tests/resources/`. + * Lists all Python files in the given root directory. * - * @param pathRelativeToResources The root directory relative to `tests/resources/`. - * @return Paths to the Python files relative to `pathRelativeToResources`. + * @param rootResourceName The resource name of the root directory. + * @return URIs of the discovered Python files. */ -export const listPythonResources = (pathRelativeToResources: string): string[] => { +export const listPythonFiles = (rootResourceName: ResourceName): URI[] => { const pattern = `**/*.py`; - const cwd = resolvePathRelativeToResources(pathRelativeToResources); + const cwd = resourceNameToUri(rootResourceName).fsPath; - return globSync(pattern, { cwd, nodir: true }); + return globSync(pattern, { cwd, nodir: true }).map((it) => URI.file(path.join(cwd, it))); }; /** - * Lists all Safe-DS files in the given directory relative to `tests/resources/` that are not skipped. The result is - * grouped by the parent directory. + * Lists all Safe-DS files in the given root directory that are not skipped. The result is grouped by the parent + * directory. * - * @param pathRelativeToResources The root directory relative to `tests/resources/`. - * @return Paths to the Safe-DS files relative to `pathRelativeToResources` grouped by the parent directory. + * @param rootResourceName The resource name of the root directory. + * @return URIs of the discovered Safe-DS files grouped by the parent directory. */ -export const listTestsResourcesGroupedByParentDirectory = ( - pathRelativeToResources: string, -): Record => { - const paths = listSafeDSResources(pathRelativeToResources); - return group(paths, (p) => path.dirname(p)) as Record; +export const listSafeDsFilesGroupedByParentDirectory = (rootResourceName: ResourceName): [URI, URI[]][] => { + const uris = listSafeDsFiles(rootResourceName); + const groupedByParentDirectory = group(uris, (p) => path.dirname(p.fsPath)) as Record; + + const result: [URI, URI[]][] = []; + for (const [parentDirectory, urisInParentDirectory] of Object.entries(groupedByParentDirectory)) { + result.push([URI.file(parentDirectory), urisInParentDirectory]); + } + + return result; }; const isNotSkipped = (pathRelativeToResources: string) => { diff --git a/tests/language/formatting/creator.ts b/tests/language/formatting/creator.ts index 3c5d38851..35679050b 100644 --- a/tests/language/formatting/creator.ts +++ b/tests/language/formatting/creator.ts @@ -1,29 +1,27 @@ -import { listSafeDSResources, resolvePathRelativeToResources } from '../../helpers/testResources.js'; -import path from 'path'; +import { listSafeDsFiles, uriToShortenedResourceName } from '../../helpers/testResources.js'; import fs from 'fs'; import { Diagnostic } from 'vscode-languageserver-types'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, URI } from 'langium'; import { getSyntaxErrors } from '../../helpers/diagnostics.js'; import { TestDescription } from '../../helpers/testDescription.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; -const root = 'formatting'; +const rootResourceName = 'formatting'; const separator = '// -----------------------------------------------------------------------------'; export const createFormattingTests = async (): Promise => { - const testCases = listSafeDSResources(root).map(createFormattingTest); + const testCases = listSafeDsFiles(rootResourceName).map(createFormattingTest); return Promise.all(testCases); }; -const createFormattingTest = async (relativeResourcePath: string): Promise => { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const program = fs.readFileSync(absolutePath).toString(); +const createFormattingTest = async (uri: URI): Promise => { + const program = fs.readFileSync(uri.fsPath).toString(); const parts = program.split(separator); // Must contain exactly one separator if (parts.length !== 2) { - return invalidTest(relativeResourcePath, new SeparatorError(parts.length - 1)); + return invalidTest(uri, new SeparatorError(parts.length - 1)); } const originalCode = normalizeLineBreaks(parts[0]).trimEnd(); @@ -32,20 +30,18 @@ const createFormattingTest = async (relativeResourcePath: string): Promise 0) { - return invalidTest(relativeResourcePath, new SyntaxErrorsInOriginalCodeError(syntaxErrorsInOriginalCode)); + return invalidTest(uri, new SyntaxErrorsInOriginalCodeError(syntaxErrorsInOriginalCode)); } // Expected formatted code must not contain syntax errors const syntaxErrorsInExpectedFormattedCode = await getSyntaxErrors(services, expectedFormattedCode); if (syntaxErrorsInExpectedFormattedCode.length > 0) { - return invalidTest( - relativeResourcePath, - new SyntaxErrorsInExpectedFormattedCodeError(syntaxErrorsInExpectedFormattedCode), - ); + return invalidTest(uri, new SyntaxErrorsInExpectedFormattedCodeError(syntaxErrorsInExpectedFormattedCode)); } + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); return { - testName: `${relativeResourcePath} should be formatted correctly`, + testName: `[${shortenedResourceName}] should be formatted correctly`, originalCode, expectedFormattedCode, }; @@ -54,12 +50,13 @@ const createFormattingTest = async (relativeResourcePath: string): Promise { +const invalidTest = (uri: URI, error: Error): FormattingTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); return { - testName: `INVALID TEST FILE [${relativeResourcePath}]`, + testName: `INVALID TEST FILE [${shortenedResourceName}]`, originalCode: '', expectedFormattedCode: '', error, diff --git a/tests/language/generation/creator.ts b/tests/language/generation/creator.ts index b34d80b29..fddaff810 100644 --- a/tests/language/generation/creator.ts +++ b/tests/language/generation/creator.ts @@ -1,68 +1,55 @@ import { - listPythonResources, - listTestsResourcesGroupedByParentDirectory, - resolvePathRelativeToResources, + listPythonFiles, + listSafeDsFilesGroupedByParentDirectory, + uriToShortenedResourceName, } from '../../helpers/testResources.js'; import path from 'path'; import fs from 'fs'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { ErrorsInCodeError, getErrors } from '../../helpers/diagnostics.js'; -import { URI } from 'vscode-uri'; import { findTestChecks } from '../../helpers/testChecks.js'; import { Location } from 'vscode-languageserver'; import { NodeFileSystem } from 'langium/node'; import { TestDescription } from '../../helpers/testDescription.js'; import { locationToString } from '../../helpers/location.js'; +import { URI } from 'langium'; const services = createSafeDsServices(NodeFileSystem).SafeDs; await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); -const root = 'generation'; +const rootResourceName = 'generation'; export const createGenerationTests = async (): Promise => { - const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); - const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => - createGenerationTest(dirname, paths), - ); + const filesGroupedByParentDirectory = listSafeDsFilesGroupedByParentDirectory(rootResourceName); + const testCases = filesGroupedByParentDirectory.map((entry) => createGenerationTest(...entry)); return Promise.all(testCases); }; -const createGenerationTest = async ( - relativeParentDirectoryPath: string, - relativeResourcePaths: string[], -): Promise => { - const inputUris: string[] = []; - const expectedOutputRoot = path.join(root, relativeParentDirectoryPath, 'output'); - const actualOutputRoot = resolvePathRelativeToResources(path.join(root, relativeParentDirectoryPath, 'generated')); +const createGenerationTest = async (parentDirectory: URI, inputUris: URI[]): Promise => { + const expectedOutputRoot = URI.file(path.join(parentDirectory.fsPath, 'output')); + const actualOutputRoot = URI.file(path.join(parentDirectory.fsPath, 'generated')); const expectedOutputFiles = readExpectedOutputFiles(expectedOutputRoot, actualOutputRoot); let runUntil: Location | undefined; - for (const relativeResourcePath of relativeResourcePaths) { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const uri = URI.file(absolutePath).toString(); - inputUris.push(uri); - - const code = fs.readFileSync(absolutePath).toString(); + for (const uri of inputUris) { + const code = fs.readFileSync(uri.fsPath).toString(); // File must not contain any errors const errors = await getErrors(services, code); if (errors.length > 0) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new ErrorsInCodeError(errors)); + return invalidTest('FILE', uri, new ErrorsInCodeError(errors)); } const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true }); // Something went wrong when finding test checks if (checksResult.isErr) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + return invalidTest('FILE', uri, checksResult.error); } // Must contain at most one comment if (checksResult.value.length > 1) { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new MultipleChecksError(checksResult.value.length), - ); + return invalidTest('FILE', uri, new MultipleChecksError(checksResult.value.length)); } // Comment must match the expected format @@ -71,27 +58,22 @@ const createGenerationTest = async ( // Expected unresolved reference if (check.comment !== 'run_until') { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new InvalidCommentError(check.comment), - ); + return invalidTest('FILE', uri, new InvalidCommentError(check.comment)); } } // Must not contain multiple run_until locations in various files const newRunUntil = checksResult.value[0]?.location; if (runUntil && newRunUntil) { - return invalidTest( - `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, - new MultipleRunUntilLocationsError([runUntil, newRunUntil]), - ); + return invalidTest('SUITE', parentDirectory, new MultipleRunUntilLocationsError([runUntil, newRunUntil])); } runUntil = newRunUntil; } + const shortenedResourceName = uriToShortenedResourceName(parentDirectory, rootResourceName); return { - testName: `[${relativeParentDirectoryPath}] should be generated correctly`, + testName: `[${shortenedResourceName}] should be generated correctly`, inputUris, actualOutputRoot, expectedOutputFiles, @@ -99,33 +81,35 @@ const createGenerationTest = async ( }; }; -const readExpectedOutputFiles = (expectedOutputRoot: string, actualOutputRoot: string): ExpectedOutputFile[] => { - const relativeResourcePaths = listPythonResources(expectedOutputRoot); - const expectedOutputFiles: ExpectedOutputFile[] = []; - - for (const relativeResourcePath of relativeResourcePaths) { - const absolutePath = resolvePathRelativeToResources(path.join(expectedOutputRoot, relativeResourcePath)); - const code = fs.readFileSync(absolutePath).toString(); - expectedOutputFiles.push({ - absolutePath: path.join(actualOutputRoot, relativeResourcePath), - content: code, - }); - } - - return expectedOutputFiles; +/** + * Read all expected output files. + * + * @param expectedOutputRoot Where the expected output files are located. + * @param actualOutputRoot Where the actual output files supposed to be located. + */ +const readExpectedOutputFiles = (expectedOutputRoot: URI, actualOutputRoot: URI): ExpectedOutputFile[] => { + return listPythonFiles(uriToShortenedResourceName(expectedOutputRoot)).map((uri) => { + return { + uri: URI.file(path.join(actualOutputRoot.fsPath, path.relative(expectedOutputRoot.fsPath, uri.fsPath))), + code: fs.readFileSync(uri.fsPath).toString(), + }; + }); }; /** * Report a test that has errors. * - * @param testName The name of the test. + * @param level Whether a test file or a test suite is invalid. + * @param uri The URI of the test file or test suite. * @param error The error that occurred. */ -const invalidTest = (testName: string, error: Error): GenerationTest => { +const invalidTest = (level: 'FILE' | 'SUITE', uri: URI, error: Error): GenerationTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); + const testName = `INVALID TEST ${level} [${shortenedResourceName}]`; return { testName, inputUris: [], - actualOutputRoot: '', + actualOutputRoot: URI.file(''), expectedOutputFiles: [], error, }; @@ -138,12 +122,12 @@ interface GenerationTest extends TestDescription { /** * The original code. */ - inputUris: string[]; + inputUris: URI[]; /** * The directory, where actual output files should be temporarily stored. */ - actualOutputRoot: string; + actualOutputRoot: URI; /** * The expected generated code. @@ -161,14 +145,14 @@ interface GenerationTest extends TestDescription { */ interface ExpectedOutputFile { /** - * Absolute path to the output file. + * URI of the output file. */ - absolutePath: string; + uri: URI; /** * Content of the output file. */ - content: string; + code: string; } /** diff --git a/tests/language/generation/testGeneration.test.ts b/tests/language/generation/testGeneration.test.ts index 29c5a9d17..2587d37db 100644 --- a/tests/language/generation/testGeneration.test.ts +++ b/tests/language/generation/testGeneration.test.ts @@ -1,7 +1,6 @@ import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { clearDocuments } from 'langium/test'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { URI } from 'langium'; import { NodeFileSystem } from 'langium/node'; import { createGenerationTests } from './creator.js'; import { SdsModule } from '../../../src/language/generated/ast.js'; @@ -29,7 +28,7 @@ describe('generation', async () => { // Load all documents const documents = test.inputUris.map((uri) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), + services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri), ); await services.shared.workspace.DocumentBuilder.build(documents); @@ -39,21 +38,21 @@ describe('generation', async () => { for (const document of documents) { const module = document.parseResult.value as SdsModule; const fileName = document.uri.fsPath; - const generatedFilePaths = generatePython(module, fileName, test.actualOutputRoot); + const generatedFilePaths = generatePython(module, fileName, test.actualOutputRoot.fsPath); actualOutputPaths.push(...generatedFilePaths); } // File paths must match - const expectedOutputPaths = test.expectedOutputFiles.map((file) => file.absolutePath).sort(); + const expectedOutputPaths = test.expectedOutputFiles.map((file) => file.uri.fsPath).sort(); expect(actualOutputPaths.sort()).toStrictEqual(expectedOutputPaths); // File contents must match for (const expectedOutputFile of test.expectedOutputFiles) { - const actualCode = fs.readFileSync(expectedOutputFile.absolutePath).toString(); - expect(actualCode).toBe(expectedOutputFile.content); + const actualCode = fs.readFileSync(expectedOutputFile.uri.fsPath).toString(); + expect(actualCode).toBe(expectedOutputFile.code); } // Remove generated files (if the test fails, the files are kept for debugging) - fs.rmSync(test.actualOutputRoot, { recursive: true, force: true }); + fs.rmSync(test.actualOutputRoot.fsPath, { recursive: true, force: true }); }); }); diff --git a/tests/language/grammar/creator.ts b/tests/language/grammar/creator.ts index 528050fb5..8543cf448 100644 --- a/tests/language/grammar/creator.ts +++ b/tests/language/grammar/creator.ts @@ -1,43 +1,43 @@ -import { listSafeDSResources, resolvePathRelativeToResources } from '../../helpers/testResources.js'; -import path from 'path'; +import { listSafeDsFiles, uriToShortenedResourceName } from '../../helpers/testResources.js'; import fs from 'fs'; import { findTestComments } from '../../helpers/testComments.js'; import { NoCommentsError } from '../../helpers/testChecks.js'; import { TestDescription } from '../../helpers/testDescription.js'; +import { URI } from 'langium'; -const root = 'grammar'; +const rootResourceName = 'grammar'; export const createGrammarTests = (): GrammarTest[] => { - return listSafeDSResources(root).map(createGrammarTest); + return listSafeDsFiles(rootResourceName).map(createGrammarTest); }; -const createGrammarTest = (relativeResourcePath: string): GrammarTest => { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const code = fs.readFileSync(absolutePath).toString(); +const createGrammarTest = (uri: URI): GrammarTest => { + const code = fs.readFileSync(uri.fsPath).toString(); const comments = findTestComments(code); // Must contain at least one comment if (comments.length === 0) { - return invalidTest(relativeResourcePath, new NoCommentsError()); + return invalidTest(uri, new NoCommentsError()); } // Must contain no more than one comment if (comments.length > 1) { - return invalidTest(relativeResourcePath, new MultipleCommentsError(comments)); + return invalidTest(uri, new MultipleCommentsError(comments)); } const comment = comments[0]; // Must contain a valid comment if (comment !== 'syntax_error' && comment !== 'no_syntax_error') { - return invalidTest(relativeResourcePath, new InvalidCommentError(comment)); + return invalidTest(uri, new InvalidCommentError(comment)); } + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); let testName: string; if (comment === 'syntax_error') { - testName = `[${relativeResourcePath}] should have syntax errors`; + testName = `[${shortenedResourceName}] should have syntax errors`; } else { - testName = `[${relativeResourcePath}] should not have syntax errors`; + testName = `[${shortenedResourceName}] should not have syntax errors`; } return { @@ -50,12 +50,13 @@ const createGrammarTest = (relativeResourcePath: string): GrammarTest => { /** * Report a test that has errors. * - * @param relativeResourcePath The path to the test file relative to the `resources` directory. + * @param uri The URI of the test file. * @param error The error that occurred. */ -const invalidTest = (relativeResourcePath: string, error: Error): GrammarTest => { +const invalidTest = (uri: URI, error: Error): GrammarTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); return { - testName: `INVALID TEST FILE [${relativeResourcePath}]`, + testName: `INVALID TEST FILE [${shortenedResourceName}]`, code: '', expectedResult: 'invalid', error, diff --git a/tests/language/partialEvaluation/creator.ts b/tests/language/partialEvaluation/creator.ts index db58539f1..83c8e1467 100644 --- a/tests/language/partialEvaluation/creator.ts +++ b/tests/language/partialEvaluation/creator.ts @@ -1,59 +1,41 @@ -import { - listTestsResourcesGroupedByParentDirectory, - resolvePathRelativeToResources, -} from '../../helpers/testResources.js'; -import path from 'path'; +import { listSafeDsFilesGroupedByParentDirectory, uriToShortenedResourceName } from '../../helpers/testResources.js'; import fs from 'fs'; import { findTestChecks } from '../../helpers/testChecks.js'; import { Location } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, URI } from 'langium'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { TestDescription } from '../../helpers/testDescription.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; -const root = 'partial evaluation'; +const rootResourceName = 'partial evaluation'; export const createPartialEvaluationTests = (): Promise => { - const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); - const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => - createPartialEvaluationTest(dirname, paths), - ); + const filesGroupedByParentDirectory = listSafeDsFilesGroupedByParentDirectory(rootResourceName); + const testCases = filesGroupedByParentDirectory.map((entry) => createPartialEvaluationTest(...entry)); return Promise.all(testCases); }; -const createPartialEvaluationTest = async ( - relativeParentDirectoryPath: string, - relativeResourcePaths: string[], -): Promise => { - const uris: string[] = []; +const createPartialEvaluationTest = async (parentDirectory: URI, uris: URI[]): Promise => { const groupIdToLocations: Map = new Map(); const serializationAssertions: SerializationAssertion[] = []; const undefinedAssertions: UndefinedAssertion[] = []; - for (const relativeResourcePath of relativeResourcePaths) { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const uri = URI.file(absolutePath).toString(); - uris.push(uri); - - const code = fs.readFileSync(absolutePath).toString(); + for (const uri of uris) { + const code = fs.readFileSync(uri.fsPath).toString(); // File must not contain any syntax errors const syntaxErrors = await getSyntaxErrors(services, code); if (syntaxErrors.length > 0) { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new SyntaxErrorsInCodeError(syntaxErrors), - ); + return invalidTest('FILE', uri, new SyntaxErrorsInCodeError(syntaxErrors)); } const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true }); // Something went wrong when finding test checks if (checksResult.isErr) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + return invalidTest('FILE', uri, checksResult.error); } for (const check of checksResult.value) { @@ -87,22 +69,20 @@ const createPartialEvaluationTest = async ( continue; } - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidCommentError(check.comment)); + return invalidTest('FILE', uri, new InvalidCommentError(check.comment)); } } // Check that all equivalence classes have at least two locations for (const [id, locations] of groupIdToLocations) { if (locations.length < 2) { - return invalidTest( - `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, - new SingletonEquivalenceClassError(id), - ); + return invalidTest('SUITE', parentDirectory, new SingletonEquivalenceClassError(id)); } } + const shortenedResourceName = uriToShortenedResourceName(parentDirectory, rootResourceName); return { - testName: `[${relativeParentDirectoryPath}] should be partially evaluated correctly`, + testName: `[${shortenedResourceName}] should be partially evaluated correctly`, uris, equivalenceClassAssertions: [...groupIdToLocations.values()].map((locations) => ({ locations })), serializationAssertions, @@ -113,10 +93,13 @@ const createPartialEvaluationTest = async ( /** * Report a test that has errors. * - * @param testName The name of the test. + * @param level Whether a test file or a test suite is invalid. + * @param uri The URI of the test file or test suite. * @param error The error that occurred. */ -const invalidTest = (testName: string, error: Error): PartialEvaluationTest => { +const invalidTest = (level: 'FILE' | 'SUITE', uri: URI, error: Error): PartialEvaluationTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); + const testName = `INVALID TEST ${level} [${shortenedResourceName}]`; return { testName, uris: [], @@ -134,7 +117,7 @@ interface PartialEvaluationTest extends TestDescription { /** * The URIs of the files that should be loaded into the workspace. */ - uris: string[]; + uris: URI[]; /** * All nodes in an equivalence class should evaluate to the same constant expression. diff --git a/tests/language/partialEvaluation/testPartialEvaluation.test.ts b/tests/language/partialEvaluation/testPartialEvaluation.test.ts index b96e66a3a..9a0617945 100644 --- a/tests/language/partialEvaluation/testPartialEvaluation.test.ts +++ b/tests/language/partialEvaluation/testPartialEvaluation.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, it } from 'vitest'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -import { URI } from 'vscode-uri'; import { NodeFileSystem } from 'langium/node'; import { clearDocuments } from 'langium/test'; import { AssertionError } from 'assert'; @@ -24,9 +23,7 @@ describe('partial evaluation', async () => { } // Load all documents - const documents = test.uris.map((uri) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), - ); + const documents = test.uris.map((uri) => services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri)); await services.shared.workspace.DocumentBuilder.build(documents); // Ensure all nodes in the equivalence class get evaluated to the same constant expression diff --git a/tests/language/scoping/creator.ts b/tests/language/scoping/creator.ts index ab64a47d7..57d0e0360 100644 --- a/tests/language/scoping/creator.ts +++ b/tests/language/scoping/creator.ts @@ -1,58 +1,40 @@ -import { - listTestsResourcesGroupedByParentDirectory, - resolvePathRelativeToResources, -} from '../../helpers/testResources.js'; -import path from 'path'; +import { listSafeDsFilesGroupedByParentDirectory, uriToShortenedResourceName } from '../../helpers/testResources.js'; import fs from 'fs'; import { findTestChecks } from '../../helpers/testChecks.js'; import { Location } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, URI } from 'langium'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { TestDescription } from '../../helpers/testDescription.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; -const root = 'scoping'; +const rootResourceName = 'scoping'; export const createScopingTests = (): Promise => { - const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); - const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => - createScopingTest(dirname, paths), - ); + const filesGroupedByParentDirectory = listSafeDsFilesGroupedByParentDirectory(rootResourceName); + const testCases = filesGroupedByParentDirectory.map((entry) => createScopingTest(...entry)); return Promise.all(testCases); }; -const createScopingTest = async ( - relativeParentDirectoryPath: string, - relativeResourcePaths: string[], -): Promise => { - const uris: string[] = []; +const createScopingTest = async (parentDirectory: URI, uris: URI[]): Promise => { const references: ExpectedReferenceWithTargetId[] = []; const targets: Map = new Map(); - for (const relativeResourcePath of relativeResourcePaths) { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const uri = URI.file(absolutePath).toString(); - uris.push(uri); - - const code = fs.readFileSync(absolutePath).toString(); + for (const uri of uris) { + const code = fs.readFileSync(uri.fsPath).toString(); // File must not contain any syntax errors const syntaxErrors = await getSyntaxErrors(services, code); if (syntaxErrors.length > 0) { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new SyntaxErrorsInCodeError(syntaxErrors), - ); + return invalidTest('FILE', uri, new SyntaxErrorsInCodeError(syntaxErrors)); } const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true }); // Something went wrong when finding test checks if (checksResult.isErr) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + return invalidTest('FILE', uri, checksResult.error); } for (const check of checksResult.value) { @@ -80,10 +62,7 @@ const createScopingTest = async ( const id = targetMatch.groups!.id!; if (targets.has(id)) { - return invalidTest( - `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, - new DuplicateTargetIdError(id), - ); + return invalidTest('SUITE', parentDirectory, new DuplicateTargetIdError(id)); } else { targets.set(id, { id, @@ -93,7 +72,7 @@ const createScopingTest = async ( continue; } - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidCommentError(check.comment)); + return invalidTest('FILE', uri, new InvalidCommentError(check.comment)); } } @@ -101,18 +80,16 @@ const createScopingTest = async ( for (const reference of references) { if (reference.targetId) { if (!targets.has(reference.targetId)) { - return invalidTest( - `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, - new MissingTargetError(reference.targetId), - ); + return invalidTest('SUITE', parentDirectory, new MissingTargetError(reference.targetId)); } reference.targetLocation = targets.get(reference.targetId)!.location; } } + const shortenedResourceName = uriToShortenedResourceName(parentDirectory, rootResourceName); return { - testName: `[${relativeParentDirectoryPath}] should be scoped correctly`, + testName: `[${shortenedResourceName}] should be scoped correctly`, uris, expectedReferences: references, }; @@ -121,10 +98,13 @@ const createScopingTest = async ( /** * Report a test that has errors. * - * @param testName The name of the test. + * @param level Whether a test file or a test suite is invalid. + * @param uri The URI of the test file or test suite. * @param error The error that occurred. */ -const invalidTest = (testName: string, error: Error): ScopingTest => { +const invalidTest = (level: 'FILE' | 'SUITE', uri: URI, error: Error): ScopingTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); + const testName = `INVALID TEST ${level} [${shortenedResourceName}]`; return { testName, uris: [], @@ -140,7 +120,7 @@ interface ScopingTest extends TestDescription { /** * The URIs of the files that should be loaded into the workspace. */ - uris: string[]; + uris: URI[]; /** * The references we expect to find in the workspace. It is allowed to have additional references, which will not be diff --git a/tests/language/scoping/testScoping.test.ts b/tests/language/scoping/testScoping.test.ts index b63107ca1..7d31c8119 100644 --- a/tests/language/scoping/testScoping.test.ts +++ b/tests/language/scoping/testScoping.test.ts @@ -28,9 +28,7 @@ describe('scoping', async () => { } // Load all documents - const documents = test.uris.map((uri) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), - ); + const documents = test.uris.map((uri) => services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri)); await services.shared.workspace.DocumentBuilder.build(documents); // Ensure all expected references match diff --git a/tests/language/typing/creator.ts b/tests/language/typing/creator.ts index f1ee8e5ed..49342337a 100644 --- a/tests/language/typing/creator.ts +++ b/tests/language/typing/creator.ts @@ -1,58 +1,40 @@ -import { - listTestsResourcesGroupedByParentDirectory, - resolvePathRelativeToResources, -} from '../../helpers/testResources.js'; -import path from 'path'; +import { listSafeDsFilesGroupedByParentDirectory, uriToShortenedResourceName } from '../../helpers/testResources.js'; import fs from 'fs'; import { findTestChecks } from '../../helpers/testChecks.js'; import { Location } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, URI } from 'langium'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { TestDescription } from '../../helpers/testDescription.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; -const root = 'typing'; +const rootResourceName = 'typing'; export const createTypingTests = (): Promise => { - const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); - const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => - createTypingTest(dirname, paths), - ); + const filesGroupedByParentDirectory = listSafeDsFilesGroupedByParentDirectory(rootResourceName); + const testCases = filesGroupedByParentDirectory.map((entry) => createTypingTest(...entry)); return Promise.all(testCases); }; -const createTypingTest = async ( - relativeParentDirectoryPath: string, - relativeResourcePaths: string[], -): Promise => { - const uris: string[] = []; +const createTypingTest = async (parentDirectory: URI, uris: URI[]): Promise => { const groupIdToLocations: Map = new Map(); const serializationAssertions: SerializationAssertion[] = []; - for (const relativeResourcePath of relativeResourcePaths) { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const uri = URI.file(absolutePath).toString(); - uris.push(uri); - - const code = fs.readFileSync(absolutePath).toString(); + for (const uri of uris) { + const code = fs.readFileSync(uri.fsPath).toString(); // File must not contain any syntax errors const syntaxErrors = await getSyntaxErrors(services, code); if (syntaxErrors.length > 0) { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new SyntaxErrorsInCodeError(syntaxErrors), - ); + return invalidTest('FILE', uri, new SyntaxErrorsInCodeError(syntaxErrors)); } const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true }); // Something went wrong when finding test checks if (checksResult.isErr) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + return invalidTest('FILE', uri, checksResult.error); } for (const check of checksResult.value) { @@ -77,22 +59,20 @@ const createTypingTest = async ( continue; } - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidCommentError(check.comment)); + return invalidTest('FILE', uri, new InvalidCommentError(check.comment)); } } // Check that all equivalence classes have at least two locations for (const [id, locations] of groupIdToLocations) { if (locations.length < 2) { - return invalidTest( - `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, - new SingletonEquivalenceClassError(id), - ); + return invalidTest('SUITE', parentDirectory, new SingletonEquivalenceClassError(id)); } } + const shortenedResourceName = uriToShortenedResourceName(parentDirectory, rootResourceName); return { - testName: `[${relativeParentDirectoryPath}] should be typed correctly`, + testName: `[${shortenedResourceName}] should be typed correctly`, uris, equivalenceClassAssertions: [...groupIdToLocations.values()].map((locations) => ({ locations })), serializationAssertions, @@ -102,10 +82,13 @@ const createTypingTest = async ( /** * Report a test that has errors. * - * @param testName The name of the test. + * @param level Whether a test file or a test suite is invalid. + * @param uri The URI of the test file or test suite. * @param error The error that occurred. */ -const invalidTest = (testName: string, error: Error): TypingTest => { +const invalidTest = (level: 'FILE' | 'SUITE', uri: URI, error: Error): TypingTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); + const testName = `INVALID TEST ${level} [${shortenedResourceName}]`; return { testName, uris: [], @@ -122,7 +105,7 @@ interface TypingTest extends TestDescription { /** * The URIs of the files that should be loaded into the workspace. */ - uris: string[]; + uris: URI[]; /** * All nodes in an equivalence class should get the same type. diff --git a/tests/language/typing/testTyping.test.ts b/tests/language/typing/testTyping.test.ts index b8a47647b..b2975c05e 100644 --- a/tests/language/typing/testTyping.test.ts +++ b/tests/language/typing/testTyping.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, it } from 'vitest'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -import { URI } from 'vscode-uri'; import { NodeFileSystem } from 'langium/node'; import { clearDocuments } from 'langium/test'; import { AssertionError } from 'assert'; @@ -28,9 +27,7 @@ describe('typing', async () => { } // Load all documents - const documents = test.uris.map((uri) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), - ); + const documents = test.uris.map((uri) => services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri)); await services.shared.workspace.DocumentBuilder.build(documents); // Ensure all nodes in the equivalence class have the same type diff --git a/tests/language/validation/creator.ts b/tests/language/validation/creator.ts index a81860fd8..fc2c9ba4e 100644 --- a/tests/language/validation/creator.ts +++ b/tests/language/validation/creator.ts @@ -1,57 +1,39 @@ -import { - listTestsResourcesGroupedByParentDirectory, - resolvePathRelativeToResources, -} from '../../helpers/testResources.js'; -import path from 'path'; +import { listSafeDsFilesGroupedByParentDirectory, uriToShortenedResourceName } from '../../helpers/testResources.js'; import fs from 'fs'; import { findTestChecks } from '../../helpers/testChecks.js'; -import { URI } from 'vscode-uri'; import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, URI } from 'langium'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -import { DocumentUri, Range } from 'vscode-languageserver-types'; +import { Range } from 'vscode-languageserver-types'; import { TestDescription } from '../../helpers/testDescription.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; -const root = 'validation'; +const rootResourceName = 'validation'; export const createValidationTests = (): Promise => { - const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); - const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => - createValidationTest(dirname, paths), - ); + const filesGroupedByParentDirectory = listSafeDsFilesGroupedByParentDirectory(rootResourceName); + const testCases = filesGroupedByParentDirectory.map((entry) => createValidationTest(...entry)); return Promise.all(testCases); }; -const createValidationTest = async ( - relativeParentDirectoryPath: string, - relativeResourcePaths: string[], -): Promise => { - const uris: string[] = []; +const createValidationTest = async (parentDirectory: URI, uris: URI[]): Promise => { const issues: ExpectedIssue[] = []; - for (const relativeResourcePath of relativeResourcePaths) { - const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); - const uri = URI.file(absolutePath).toString(); - uris.push(uri); - - const code = fs.readFileSync(absolutePath).toString(); + for (const uri of uris) { + const code = fs.readFileSync(uri.fsPath).toString(); // File must not contain any syntax errors const syntaxErrors = await getSyntaxErrors(services, code); if (syntaxErrors.length > 0) { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new SyntaxErrorsInCodeError(syntaxErrors), - ); + return invalidTest(uri, new SyntaxErrorsInCodeError(syntaxErrors)); } const checksResult = findTestChecks(code, uri); // Something went wrong when finding test checks if (checksResult.isErr) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + return invalidTest(uri, checksResult.error); } for (const check of checksResult.value) { @@ -60,10 +42,7 @@ const createValidationTest = async ( // Overall comment is invalid if (!match) { - return invalidTest( - `INVALID TEST FILE [${relativeResourcePath}]`, - new InvalidCommentError(check.comment), - ); + return invalidTest(uri, new InvalidCommentError(check.comment)); } // Extract groups from the match @@ -74,7 +53,7 @@ const createValidationTest = async ( // Validate the severity if (!validSeverities.includes(severity as any)) { - return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidSeverityError(severity)); + return invalidTest(uri, new InvalidSeverityError(severity)); } // Add the issue @@ -89,8 +68,9 @@ const createValidationTest = async ( } } + const shortenedResourceName = uriToShortenedResourceName(parentDirectory, rootResourceName); return { - testName: `[${relativeParentDirectoryPath}] should be validated correctly`, + testName: `[${shortenedResourceName}] should be validated correctly`, uris, expectedIssues: issues, }; @@ -99,12 +79,14 @@ const createValidationTest = async ( /** * Report a test that has errors. * - * @param relativeResourcePath The path to the test file relative to the `resources` directory. + * @param uri The URI of the test file. * @param error The error that occurred. */ -const invalidTest = (relativeResourcePath: string, error: Error): ValidationTest => { +const invalidTest = (uri: URI, error: Error): ValidationTest => { + const shortenedResourceName = uriToShortenedResourceName(uri, rootResourceName); + const testName = `INVALID TEST FILE [${shortenedResourceName}]`; return { - testName: `INVALID TEST FILE [${relativeResourcePath}]`, + testName, uris: [], expectedIssues: [], error, @@ -118,7 +100,7 @@ interface ValidationTest extends TestDescription { /** * The URIs of the files that should be loaded into the workspace. */ - uris: string[]; + uris: URI[]; /** * The issues we expect to find in the workspace. @@ -153,7 +135,7 @@ export interface ExpectedIssue { /** * The URI of the file containing the issue. */ - uri: DocumentUri; + uri: URI; /** * The range of the issue. If undefined, the issue is expected to be present in the whole file. diff --git a/tests/language/validation/testValidation.test.ts b/tests/language/validation/testValidation.test.ts index 0b8175ed1..371037224 100644 --- a/tests/language/validation/testValidation.test.ts +++ b/tests/language/validation/testValidation.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, it } from 'vitest'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -import { URI } from 'vscode-uri'; import { NodeFileSystem } from 'langium/node'; import { createValidationTests, ExpectedIssue } from './creator.js'; import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; @@ -27,9 +26,7 @@ describe('validation', async () => { } // Load all documents - const documents = test.uris.map((uri) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), - ); + const documents = test.uris.map((uri) => services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri)); await services.shared.workspace.DocumentBuilder.build(documents, { validation: true }); // Ensure all expected issues match @@ -75,7 +72,7 @@ describe('validation', async () => { * @param expectedIssue The expected issue. */ const getMatchingActualIssues = (expectedIssue: ExpectedIssue): Diagnostic[] => { - const document = services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(expectedIssue.uri)); + const document = services.shared.workspace.LangiumDocuments.getOrCreateDocument(expectedIssue.uri); let result = document.diagnostics ?? []; // Filter by severity @@ -119,7 +116,7 @@ const getMatchingActualIssues = (expectedIssue: ExpectedIssue): Diagnostic[] => */ const issueLocationToString = (expectedIssue: ExpectedIssue): string => { if (expectedIssue.range) { - return `at ${locationToString({ uri: expectedIssue.uri, range: expectedIssue.range })}`; + return `at ${locationToString({ uri: expectedIssue.uri.toString(), range: expectedIssue.range })}`; } else { return `in ${expectedIssue.uri}`; } diff --git a/tests/resources/helpers/listTestResources/nested/not a safe-ds file.txt b/tests/resources/helpers/listPythonFiles/nested/not a python file.txt similarity index 100% rename from tests/resources/helpers/listTestResources/nested/not a safe-ds file.txt rename to tests/resources/helpers/listPythonFiles/nested/not a python file.txt diff --git a/tests/resources/helpers/listTestResources/nested/pipeline file.sdspipe b/tests/resources/helpers/listPythonFiles/nested/python file.py similarity index 100% rename from tests/resources/helpers/listTestResources/nested/pipeline file.sdspipe rename to tests/resources/helpers/listPythonFiles/nested/python file.py diff --git a/tests/resources/helpers/listTestResources/nested/skip pipeline file.sdspipe b/tests/resources/helpers/listPythonFiles/not a python file.txt similarity index 100% rename from tests/resources/helpers/listTestResources/nested/skip pipeline file.sdspipe rename to tests/resources/helpers/listPythonFiles/not a python file.txt diff --git a/tests/resources/helpers/listTestResources/nested/skip stub file.sdsstub b/tests/resources/helpers/listPythonFiles/python file.py similarity index 100% rename from tests/resources/helpers/listTestResources/nested/skip stub file.sdsstub rename to tests/resources/helpers/listPythonFiles/python file.py diff --git a/tests/resources/helpers/listTestResources/not a safe-ds file.txt b/tests/resources/helpers/listSafeDsFiles/nested/not a safe-ds file.txt similarity index 100% rename from tests/resources/helpers/listTestResources/not a safe-ds file.txt rename to tests/resources/helpers/listSafeDsFiles/nested/not a safe-ds file.txt diff --git a/tests/resources/helpers/listTestResources/pipeline file.sdspipe b/tests/resources/helpers/listSafeDsFiles/nested/pipeline file.sdspipe similarity index 100% rename from tests/resources/helpers/listTestResources/pipeline file.sdspipe rename to tests/resources/helpers/listSafeDsFiles/nested/pipeline file.sdspipe diff --git a/tests/resources/helpers/listTestResources/skip pipeline file.sdspipe b/tests/resources/helpers/listSafeDsFiles/nested/skip pipeline file.sdspipe similarity index 100% rename from tests/resources/helpers/listTestResources/skip pipeline file.sdspipe rename to tests/resources/helpers/listSafeDsFiles/nested/skip pipeline file.sdspipe diff --git a/tests/resources/helpers/listTestResources/skip stub file.sdsstub b/tests/resources/helpers/listSafeDsFiles/nested/skip stub file.sdsstub similarity index 100% rename from tests/resources/helpers/listTestResources/skip stub file.sdsstub rename to tests/resources/helpers/listSafeDsFiles/nested/skip stub file.sdsstub diff --git a/tests/resources/helpers/listTestResources/nested/skip test file.sdstest b/tests/resources/helpers/listSafeDsFiles/nested/skip test file.sdstest similarity index 100% rename from tests/resources/helpers/listTestResources/nested/skip test file.sdstest rename to tests/resources/helpers/listSafeDsFiles/nested/skip test file.sdstest diff --git a/tests/resources/helpers/listTestResources/nested/stub file.sdsstub b/tests/resources/helpers/listSafeDsFiles/nested/stub file.sdsstub similarity index 100% rename from tests/resources/helpers/listTestResources/nested/stub file.sdsstub rename to tests/resources/helpers/listSafeDsFiles/nested/stub file.sdsstub diff --git a/tests/resources/helpers/listTestResources/nested/test file.sdstest b/tests/resources/helpers/listSafeDsFiles/nested/test file.sdstest similarity index 100% rename from tests/resources/helpers/listTestResources/nested/test file.sdstest rename to tests/resources/helpers/listSafeDsFiles/nested/test file.sdstest diff --git a/tests/resources/helpers/listTestResources/skip test file.sdstest b/tests/resources/helpers/listSafeDsFiles/not a safe-ds file.txt similarity index 100% rename from tests/resources/helpers/listTestResources/skip test file.sdstest rename to tests/resources/helpers/listSafeDsFiles/not a safe-ds file.txt diff --git a/tests/resources/helpers/listTestResources/stub file.sdsstub b/tests/resources/helpers/listSafeDsFiles/pipeline file.sdspipe similarity index 100% rename from tests/resources/helpers/listTestResources/stub file.sdsstub rename to tests/resources/helpers/listSafeDsFiles/pipeline file.sdspipe diff --git a/tests/resources/helpers/listTestResources/test file.sdstest b/tests/resources/helpers/listSafeDsFiles/skip pipeline file.sdspipe similarity index 100% rename from tests/resources/helpers/listTestResources/test file.sdstest rename to tests/resources/helpers/listSafeDsFiles/skip pipeline file.sdspipe diff --git a/tests/resources/helpers/listSafeDsFiles/skip stub file.sdsstub b/tests/resources/helpers/listSafeDsFiles/skip stub file.sdsstub new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/helpers/listSafeDsFiles/skip test file.sdstest b/tests/resources/helpers/listSafeDsFiles/skip test file.sdstest new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/helpers/listSafeDsFiles/stub file.sdsstub b/tests/resources/helpers/listSafeDsFiles/stub file.sdsstub new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/helpers/listSafeDsFiles/test file.sdstest b/tests/resources/helpers/listSafeDsFiles/test file.sdstest new file mode 100644 index 000000000..e69de29bb