UNPKG

typir

Version:

General purpose type checking library

237 lines (224 loc) 13 kB
/****************************************************************************** * Copyright 2024 TypeFox GmbH * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ import { expect } from 'vitest'; import { Type } from '../graph/type-node.js'; import { Severity } from '../services/validation.js'; import { TypirServices, TypirSpecifics } from '../typir.js'; /** * Testing utility to check, that exactly the expected types are in the type system. * @param services the Typir services * @param filterTypes used to identify the types of interest * @param namesOfExpectedTypes the names (not the identifiers!) of the expected types; * ensures that there are no more types; * it is possible to specify names multiple times, if there are multiple types with the same name (e.g. for overloaded functions) * @returns all the found types */ export function expectTypirTypes<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): Type[] { const types = services.infrastructure.Graph.getAllRegisteredTypes().filter(filterTypes); types.forEach(type => expect(type.getInitializationState()).toBe('Completed')); // check that all types are 'Completed' const typeNames = types.map(t => t.getName()); expect(typeNames, typeNames.join(', ')).toHaveLength(namesOfExpectedTypes.length); for (const name of namesOfExpectedTypes) { const index = typeNames.indexOf(name); expect(index >= 0).toBeTruthy(); typeNames.splice(index, 1); // removing elements is needed to work correctly with duplicated entries } expect(typeNames, `There are more types than expected: ${typeNames.join(', ')}`).toHaveLength(0); return types; } export function expectToBeType<T extends Type>(type: unknown, checkType: (t: unknown) => t is T, checkDetails?: (t: T) => boolean): asserts type is T { if (checkType(type)) { if (checkDetails === undefined || checkDetails(type)) { // everything is fine } else { expect.fail(`'${type.getIdentifier()}' is the actual Typir type, but the details are wrong`); } } else { expect.fail(`'${type}' is not the expected Typir type`); } } /** * Tests, whether exactly the specified issues are found during the validation of the given language node, * i.e. neither more nor less validation issues. * @param services the Typir services * @param languageNode the language node to validate * @param expectedIssues the expected issues to occur */ export function expectValidationIssues<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], expectedIssues: string[]): void; /** * Tests, whether the specified issues are found during the validation of the given language node, * more validation issues beyond the specified ones might occur. * @param services the Typir services * @param languageNode the language node to validate * @param options These options are used to filter all occurred issues before the expectations are checked * @param expectedIssues the expected issues to occur */ export function expectValidationIssues<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], options: ExpectedValidationIssuesOptions, expectedIssues: string[]): void; export function expectValidationIssues<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], optionsOrIssues: ExpectedValidationIssuesOptions | string[], issues?: string[]): void { const expectedIssues = Array.isArray(optionsOrIssues) ? optionsOrIssues : issues ?? []; const options = Array.isArray(optionsOrIssues) ? {} : optionsOrIssues; const actualIssues = validateAndFilter(services, languageNode, options); compareValidationIssues(actualIssues, expectedIssues); } /** * Tests, whether exactly the specified issues are found during the validation of the given language node, * i.e. neither more nor less validation issues. * @param services the Typir services * @param languageNode the language node to validate * @param expectedStrictIssues the expected issues to occur */ export function expectValidationIssuesStrict<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], expectedStrictIssues: string[]): void; /** * Tests, whether exactly the specified issues are found during the validation of the given language node, * i.e. neither more nor less validation issues. * @param services the Typir services * @param languageNode the language node to validate * @param options These options are used to filter all occurred issues before the expectations are checked * @param expectedStrictIssues the expected issues to occur */ export function expectValidationIssuesStrict<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], options: ExpectedValidationIssuesOptions, expectedStrictIssues: string[]): void; export function expectValidationIssuesStrict<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], optionsOrIssues: ExpectedValidationIssuesOptions | string[], issues?: string[]): void { const expectedStrictIssues = Array.isArray(optionsOrIssues) ? optionsOrIssues : issues ?? []; const options = Array.isArray(optionsOrIssues) ? {} : optionsOrIssues; const actualIssues = validateAndFilter(services, languageNode, options); compareValidationIssuesStrict(actualIssues, expectedStrictIssues); } /** * Tests, whether the specified issues are NOT found during the validation of the given language node, * other validation issues than the specified ones might occur. * @param services the Typir services * @param languageNode the language node to validate * @param forbiddenIssues the issues which are expected to NOT occur */ export function expectValidationIssuesAbsent<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], forbiddenIssues: string[]): void; /** * Tests, whether the specified issues are NOT found during the validation of the given language node, * other validation issues than the specified ones might occur. * @param services the Typir services * @param languageNode the language node to validate * @param options These options are used to filter all occurred issues before the expectations are checked * @param forbiddenIssues the issues which are expected to NOT occur */ export function expectValidationIssuesAbsent<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], options: ExpectedValidationIssuesOptions, forbiddenIssues: string[]): void; export function expectValidationIssuesAbsent<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], optionsOrIssues: ExpectedValidationIssuesOptions | string[], issues?: string[]): void { const expectedForbiddenIssues = Array.isArray(optionsOrIssues) ? optionsOrIssues : issues ?? []; const options = Array.isArray(optionsOrIssues) ? {} : optionsOrIssues; const actualIssues = validateAndFilter(services, languageNode, options); compareValidationIssuesAbsent(actualIssues, expectedForbiddenIssues); } /** * Tests, whether no issues at all are found during the validation of the given language node. * @param services the Typir services * @param languageNode the language node to validate * @param options These options are used to filter all occurred issues before the expectations are checked */ export function expectValidationIssuesNone<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], options?: ExpectedValidationIssuesOptions): void { const optionsToUse = options ?? {}; const actualIssues = validateAndFilter(services, languageNode, optionsToUse); compareValidationIssuesNone(actualIssues); } export interface ExpectedValidationIssuesOptions { /** Check only issues which have the specified severity (or check all issues in case of an 'undefined' severity) */ severity?: Severity; // more properties for filtering might be added in the future } function validateAndFilter<Specifics extends TypirSpecifics>(services: TypirServices<Specifics>, languageNode: Specifics['LanguageType'], options: ExpectedValidationIssuesOptions): string[] { return services.validation.Collector.validate(languageNode) .filter(v => options.severity ? v.severity === options.severity : true) .map(v => services.Printer.printTypirProblem(v)); } export function compareValidationIssues(actualIssues: string[], expectedIssues: string[]): void { compareValidationIssuesLogic(actualIssues, expectedIssues); } export function compareValidationIssuesStrict(actualIssues: string[], expectedStrictIssues: string[]): void { compareValidationIssuesLogic(actualIssues, expectedStrictIssues, { strict: true }); } export function compareValidationIssuesAbsent(actualIssues: string[], forbiddenIssues: string[]): void { compareValidationIssuesLogicAbsent(actualIssues, forbiddenIssues); } export function compareValidationIssuesNone(actualIssues: string[]): void { compareValidationIssuesLogic(actualIssues, [], { strict: true }); } function compareValidationIssuesLogic(actualIssues: string[], expectedErrors: string[], options?: { strict?: boolean }): void { // compare actual and expected issues let indexExpected = 0; while (indexExpected < expectedErrors.length) { let indexActual = 0; let found = false; while (indexActual < actualIssues.length) { if (actualIssues[indexActual].includes(expectedErrors[indexExpected])) { found = true; // remove found matches => at the end, the not matching issues remain to be reported actualIssues.splice(indexActual, 1); expectedErrors.splice(indexExpected, 1); break; } indexActual++; } if (found) { // indexExpected was implicitly incremented } else { indexExpected++; } } // report the result const msgExpected = expectedErrors.join('\n').trim(); const msgActual = actualIssues.join('\n').trim(); if (msgExpected.length >= 1 && msgActual.length >= 1) { if (options?.strict) { expect.fail(`Didn't find expected issues:\n${msgExpected}\nBut found some more issues:\n${msgActual}`); } else { expect.fail(`Didn't find expected issues:\n${msgExpected}\nThese other issues are ignored:\n${msgActual}`); // printing the ignored issues help to identify typos, ... in the specified issues } } else if (msgExpected.length >= 1) { expect.fail(`Didn't find expected issues:\n${msgExpected}`); } else if (msgActual.length >= 1) { if (options?.strict) { expect.fail(`Found some more issues:\n${msgActual}`); } else { // ignore additional issues } } else { // everything is fine } } function compareValidationIssuesLogicAbsent(actualIssues: string[], forbiddenErrors: string[]): void { // compare actual and expected issues let indexExpected = 0; while (indexExpected < forbiddenErrors.length) { let indexActual = 0; let found = false; while (indexActual < actualIssues.length) { if (actualIssues[indexActual].includes(forbiddenErrors[indexExpected])) { found = true; break; } indexActual++; } if (found) { indexExpected++; } else { // remove issues which did not occur => at the end, the issues which occurred remain to be reported actualIssues.splice(indexActual, 1); forbiddenErrors.splice(indexExpected, 1); // indexExpected was implicitly incremented } } // report the result const msgForbidden = forbiddenErrors.join('\n').trim(); const msgActual = actualIssues.join('\n').trim(); if (msgForbidden.length >= 1 && msgActual.length >= 1) { expect.fail(`Found these forbidden issues:\n${msgForbidden}\nThese other issues are ignored:\n${msgActual}`); // printing the ignored issues help to identify typos, ... in the specified forbidden issues } else if (msgForbidden.length >= 1) { expect.fail(`Found these forbidden issues:\n${msgForbidden}`); } else if (msgActual.length >= 1) { // ignore additional issues } else { // everything is fine } }