From 0186685afb01eda3393914e61d64384b9d4b77e4 Mon Sep 17 00:00:00 2001 From: vadson71 Date: Wed, 10 Jan 2024 16:01:00 +0200 Subject: [PATCH] feat: fallback to latest supported patch version (#687) * fix: fallback to latest supported patch version * fix: function docs and pr comments * fix: readme file correction * fix: work on pr comments * fix: linter --- .changeset/cool-days-itch.md | 14 + .vscode/launch.json | 27 + packages/context/src/types.ts | 3 +- packages/context/src/ui5-model.ts | 518 +++++++++--- packages/context/test/unit/ui5-model.test.ts | 786 +++++++++++------- .../test/unit/ui5-model_fs-mocked.test.ts | 29 +- packages/language-server/api.d.ts | 4 +- .../unit/completion-items-classes.test.ts | 6 +- .../unit/completion-items-literals.test.ts | 10 +- .../test/unit/completion-items.test.ts | 10 +- .../test/unit/documentation.test.ts | 6 +- .../language-server/test/unit/hover.test.ts | 6 +- .../test/unit/quick-fix.test.ts | 10 +- .../xml-view-diagnostics/snapshots-utils.ts | 23 +- packages/logic-utils/src/utils/types.ts | 4 +- .../test/unit/find-classes-matching.test.ts | 33 +- .../test/unit/get-super-class.test.ts | 6 +- .../test/unit/xml-node-to-ui5-node.test.ts | 49 +- .../test/unit/api-negative.test.ts | 6 +- packages/semantic-model/test/unit/api.test.ts | 12 +- .../semantic-model/test/unit/unit.test.ts | 7 +- .../semantic-model/test/unit/utils.test.ts | 6 +- .../vscode-ui5-language-assistant/README.md | 8 +- .../test/unit/api.test.ts | 10 +- .../providers/attributeName/namespace.test.ts | 6 +- .../attributeName/prop-event-assoc.test.ts | 6 +- .../attributeValue/boolean-literal.test.ts | 10 +- .../providers/attributeValue/enum.test.ts | 10 +- .../attributeValue/namespace.test.ts | 6 +- .../providers/elementName/aggregation.test.ts | 6 +- .../providers/elementName/classes.test.ts | 10 +- .../test/unit/tooltip.test.ts | 6 +- .../test/unit/api.test.ts | 10 +- .../attributes/invalid-boolean-value.test.ts | 10 +- .../attributes/unknown-attribute-key.test.ts | 6 +- .../attributes/unknown-enum-value.test.ts | 10 +- .../unknown-xmlns-namespace.test.ts | 10 +- .../use-of-depracated-attribute.test.ts | 6 +- .../validators/document/non-unique-id.test.ts | 10 +- .../cardinality-of-aggregation.test.ts | 10 +- .../validators/element/non-stable-id.test.ts | 10 +- .../element/type-of-aggregation.test.ts | 10 +- .../use-of-deprecated-aggregation.test.ts | 10 +- .../element/use-of-deprecated-class.test.ts | 10 +- test-packages/test-utils/api.d.ts | 9 +- test-packages/test-utils/src/api.ts | 3 + .../src/utils/download-ui5-resources.ts | 44 + .../src/utils/semantic-model-provider.ts | 2 +- 48 files changed, 1270 insertions(+), 553 deletions(-) create mode 100644 .changeset/cool-days-itch.md diff --git a/.changeset/cool-days-itch.md b/.changeset/cool-days-itch.md new file mode 100644 index 000000000..4062db685 --- /dev/null +++ b/.changeset/cool-days-itch.md @@ -0,0 +1,14 @@ +--- +"@ui5-language-assistant/xml-views-completion": patch +"@ui5-language-assistant/xml-views-validation": patch +"@ui5-language-assistant/xml-views-tooltip": patch +"@ui5-language-assistant/language-server": patch +"@ui5-language-assistant/test-utils": patch +"@ui5-language-assistant/semantic-model": patch +"@ui5-language-assistant/logic-utils": patch +"@ui5-language-assistant/context": patch +"vscode-ui5-language-assistant": patch +"@ui5-language-assistant/vscode-ui5-language-assistant-bas-ext": patch +--- + +Fallback to the latest supported patch version diff --git a/.vscode/launch.json b/.vscode/launch.json index 68a82ca17..bc63a4731 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -49,6 +49,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], @@ -68,6 +69,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], @@ -87,6 +89,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], @@ -106,6 +109,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], @@ -117,6 +121,26 @@ }, "cwd": "${workspaceFolder}/packages/xml-views-validation" }, + { + "type": "node", + "request": "launch", + "name": "XML completion: Run Current Jest File", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--coverage=false", + "--maxWorkers=1", + "--config", + "jest.config.js" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + }, + "cwd": "${workspaceFolder}/packages/xml-views-completion" + }, { "type": "node", "request": "launch", @@ -125,6 +149,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], @@ -144,6 +169,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], @@ -163,6 +189,7 @@ "args": [ "${fileBasenameNoExtension}", "--coverage=false", + "--maxWorkers=1", "--config", "jest.config.js" ], diff --git a/packages/context/src/types.ts b/packages/context/src/types.ts index b57eca129..ad6b257bd 100644 --- a/packages/context/src/types.ts +++ b/packages/context/src/types.ts @@ -8,6 +8,7 @@ import { FetchResponse } from "@ui5-language-assistant/logic-utils"; export const DEFAULT_UI5_FRAMEWORK = "SAPUI5"; export const DEFAULT_UI5_VERSION = "1.71.61"; +export const DEFAULT_UI5_VERSION_BASE = "1.71"; export const UI5_VERSION_S4_PLACEHOLDER = "${sap.ui5.dist.version}"; export const UI5_FRAMEWORK_CDN_BASE_URL = { OpenUI5: "https://sdk.openui5.org/", @@ -113,4 +114,4 @@ export type CAPProjectKind = "Java" | "NodeJS"; export type ProjectKind = CAPProjectKind | "UI5"; export type Project = UI5Project | CAPProject; export type ProjectType = typeof UI5_PROJECT_TYPE | typeof CAP_PROJECT_TYPE; -export type Fetcher = (url: string) => Promise; +export type Fetcher = (url: string) => Promise>; diff --git a/packages/context/src/ui5-model.ts b/packages/context/src/ui5-model.ts index 08da4dcec..e55ad9a1f 100644 --- a/packages/context/src/ui5-model.ts +++ b/packages/context/src/ui5-model.ts @@ -1,7 +1,7 @@ import { map, get } from "lodash"; import { resolve } from "path"; import { pathExists, lstat, readJson, writeJson, mkdirs } from "fs-extra"; -import semver from "semver"; +import semver, { SemVer } from "semver"; import semverMinSatisfying from "semver/ranges/min-satisfying"; import { UI5Framework, @@ -12,8 +12,16 @@ import { Json, TypeNameFix, } from "@ui5-language-assistant/semantic-model"; -import { Fetcher, UI5_VERSION_S4_PLACEHOLDER } from "./types"; -import { fetch } from "@ui5-language-assistant/logic-utils"; +import { + DEFAULT_UI5_VERSION_BASE, + Fetcher, + UI5_VERSION_S4_PLACEHOLDER, +} from "./types"; +import { + fetch, + getLocalUrl, + tryFetch, +} from "@ui5-language-assistant/logic-utils"; import { getLibraryAPIJsonUrl, getLogger, @@ -23,6 +31,11 @@ import { import { DEFAULT_UI5_VERSION } from "./types"; import { cache } from "./cache"; +export type VersionMapJsonType = Record< + string, + { version: string; support: string; lts: boolean } +>; + export async function getSemanticModel( modelCachePath: string | undefined, framework: UI5Framework, @@ -259,7 +272,7 @@ async function getVersionInfo( const cacheFolder = getCacheFolder(modelCachePath, framework, version); cacheFilePath = getCacheFilePath(cacheFolder, "sap-ui-version.json"); } - let versionInfo = await readFromCache(cacheFilePath); + let versionInfo: unknown = await readFromCache(cacheFilePath); if (versionInfo === undefined) { const url = await getVersionInfoUrl(framework, version); try { @@ -279,14 +292,88 @@ async function getVersionInfo( return versionInfo; } -async function getLibs( +/** + * Checks whether version libs cache exists and is consistent. + * + * @param version Requested version + * @param framework The UI5 framework version + * @param modelCachePath Path to model data cache on disk + * @param libs Library module names + * @returns true if cache data exists for all libs + */ +async function isVersionLibsCacheValid( + version: string, + framework: UI5Framework, + modelCachePath: string, + libs: string[] +): Promise { + const cacheFolder = getCacheFolder(modelCachePath, framework, version); + const result = await Promise.all( + map(libs, async (libName) => { + const cacheFilePath = getCacheFilePath(cacheFolder, libName); + let apiJson: Json | undefined; + try { + apiJson = await readFromCache(cacheFilePath); + } catch (e) { + apiJson = undefined; + } + return apiJson && typeof apiJson === "object"; + }) + ); + return result.every((item) => !!item); +} + +/** + * Checks whether the test resources for specific version are available locally or on server. + * + * @param versionInfoJsonFetcher Fetcher function which loads version info json + * @param versionLibsFetcher Fetcher function which loads version libs + * @param modelCachePath Path to model data cache on disk + * @param framework The UI5 framework version + * @param version Requested version + * @returns true if resources are found + */ +async function isVersionResourcesAvailable( + versionInfoJsonFetcher: Fetcher, + versionLibsFetcher: Fetcher, modelCachePath: string | undefined, framework: UI5Framework, version: string +): Promise { + const libs = await getLibs( + modelCachePath, + framework, + version, + versionInfoJsonFetcher + ); + if (!libs) { + return false; + } + if ( + modelCachePath && + (await isVersionLibsCacheValid(version, framework, modelCachePath, libs)) + ) { + return true; + } + + // Are test resources available on server? + const url = await getLibraryAPIJsonUrl(framework, version, libs[0]); + const response = await versionLibsFetcher(url); + if (response.ok) { + return true; + } + return false; +} + +async function getLibs( + modelCachePath: string | undefined, + framework: UI5Framework, + version: string, + fetcher?: Fetcher ): Promise { let libs: string[] | undefined = undefined; const versionInfo = await getVersionInfo( - fetch, + fetcher ?? fetch, modelCachePath, framework, version @@ -301,10 +388,8 @@ async function getLibs( } // if library is not found, resolve next minor highest patch -const versionMap: Record< - UI5Framework, - Record -> = Object.create(null); // just an in-memory cache! +const versionMap: Record = + Object.create(null); // just an in-memory cache! const resolvedVersions: Record< UI5Framework, Record @@ -343,6 +428,7 @@ async function negotiateVersion( isIncorrectVersion: boolean; }> { return negotiateVersionWithFetcher( + fetch, fetch, fetch, modelCachePath, @@ -350,10 +436,24 @@ async function negotiateVersion( version ); } -// This function is exported for testing purposes (using a mock fetcher) + +/** + * Tries to negotiate UI5 version. + * Note: This function is exported for testing purposes (using a mock fetcher). + * All Three fetchers are exposed to have comprehensive and easy control over web requests in tests + * + * @param versionJsonFetcher Fetcher function which loads version map json + * @param versionInfoJsonFetcher Fetcher function which loads version info json + * @param versionLibsFetcher Fetcher function which loads version libs + * @param modelCachePath Path to model data cache on disk + * @param framework The UI5 framework version + * @param version Requested version + * @returns Object with resolved version number accompanied with flags indicating if fallback fersion is used and/or whether requested version is incorrect + */ export async function negotiateVersionWithFetcher( - versionJsonFetcher: Fetcher, + versionJsonFetcher: Fetcher, versionInfoJsonFetcher: Fetcher, + versionLibsFetcher: Fetcher, modelCachePath: string | undefined, framework: UI5Framework, version: string | undefined @@ -362,141 +462,319 @@ export async function negotiateVersionWithFetcher( isFallback: boolean; isIncorrectVersion: boolean; }> { - // try to negotiate version let isFallback = false; let isIncorrectVersion = false; let useLatestVersion = false; - let versions = versionMap[framework]; - let adjustedVersion: string = version || DEFAULT_UI5_VERSION; + const isOffline = await isOfflineMode(version); + if (isOffline) { + return { + version: version as string, + isFallback, + isIncorrectVersion, + }; + } - if (version === UI5_VERSION_S4_PLACEHOLDER) { - useLatestVersion = true; - adjustedVersion = version; - getLogger().warn( - `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the latest available version is used instead` - ); - } else if (version && !isVersionSupported(version)) { - // version is out of support in LA, using default version - getLogger().warn( - `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the fallback version ${DEFAULT_UI5_VERSION} is used instead` - ); - adjustedVersion = "1.71"; - isIncorrectVersion = true; + const versions = await getVersionsMap(framework, versionJsonFetcher); + + let adjustedVersion: string; + ({ adjustedVersion, isFallback, isIncorrectVersion, useLatestVersion } = + adjustRequestedVersion(version, versions)); + + // If version is already resolved then return it + if (resolvedVersions[framework]?.[adjustedVersion]) { + const resolvedVersion = resolvedVersions[framework][adjustedVersion]; + if (version && resolvedVersion !== adjustedVersion) { + // Resolved version is taken from cache, we need to raise the flag when version is incorrect + isIncorrectVersion = true; + } + return { + version: resolvedVersion, + isFallback, + isIncorrectVersion, + }; + } + + const requestedVersion = adjustedVersion; + + // Version resolution + const { + resolvedVersion, + isIncorrectVersion: isResolvedIncorrectVersion, + remember, + } = await resolveRequestedVersion( + requestedVersion, + useLatestVersion, + framework, + versions, + versionInfoJsonFetcher, + versionLibsFetcher, + modelCachePath + ); + + adjustedVersion = resolvedVersion; + isIncorrectVersion = + isIncorrectVersion || (!!version && isResolvedIncorrectVersion); + + // store the resolved version + if (remember) { + if (version && requestedVersion !== adjustedVersion) { + isIncorrectVersion = true; + } + if (!resolvedVersions[framework]) { + resolvedVersions[framework] = {}; + } + resolvedVersions[framework][requestedVersion] = adjustedVersion; } + return { + version: adjustedVersion, + isFallback, + isIncorrectVersion, + }; +} + +async function isOfflineMode(version: string | undefined): Promise { + if (!version) { + return false; + } + const localUrl = getLocalUrl(version); + if (localUrl) { + const response = await tryFetch(localUrl); + if (response) { + return true; + } + } + return false; +} + +// Versions 1.38 and older are not supported because of missing features in the API, while the LA relies on that API for validation. +// See https://github.com/SAP/ui5-language-assistant/issues/538 for details +function isVersionSupported(version: string | undefined): { + result: boolean; + versionDetails: SemVer | null; +} { + const versionDetails = semver.coerce(version); + if (!versionDetails) { + return { result: false, versionDetails }; + } + return { + result: !( + versionDetails.major === 1 && + versionDetails.minor && + versionDetails.minor <= 38 + ), + versionDetails, + }; +} + +/** + * Performs preliminary version adjustments for special use cases (empty version, S4 placeholder, out of support). + * + * @param version Version to adjust + * @returns Object with adjusted version and flags to indicate the fallback is used or version is incorrect or latest available is required + */ +function adjustRequestedVersion( + version: string | undefined, + versions: VersionMapJsonType +): { + adjustedVersion: string; + isFallback: boolean; + isIncorrectVersion: boolean; + useLatestVersion: boolean; +} { + const logger = getLogger(); + let isFallback = false; + let isIncorrectVersion = false; + let useLatestVersion = false; + + if (versions["latest"]?.version === DEFAULT_UI5_VERSION) { + // version map download failed, default fallback version is set as latest + isFallback = true; + } + + // Dynamic fallback version determination + const fallbackVersion = + versions[DEFAULT_UI5_VERSION_BASE]?.version || DEFAULT_UI5_VERSION; + + let adjustedVersion: string = version || fallbackVersion; + if (!version) { // no version defined, using default version - getLogger().warn( + logger.warn( "No version defined! Please check the minUI5Version in your manifest.json!" ); isFallback = true; - } else if (resolvedVersions[framework]?.[adjustedVersion]) { - // version already resolved? - const versionDefined = adjustedVersion; - adjustedVersion = resolvedVersions[framework]?.[adjustedVersion]; - if (versionDefined !== adjustedVersion) { - isIncorrectVersion = true; + } else if (version === UI5_VERSION_S4_PLACEHOLDER) { + // handling for S4 placeholder + logger.warn( + `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the latest available version is used instead` + ); + useLatestVersion = true; + } else if (version) { + const { result, versionDetails } = isVersionSupported(version); + if (!result) { + if (versionDetails) { + // version is out of support in LA, using default version + logger.warn( + `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the fallback version ${fallbackVersion} is used instead` + ); + adjustedVersion = fallbackVersion; + isIncorrectVersion = true; + } else { + // version is incorrect, using latest version + logger.warn( + `The version specified as minUI5Version in your manifest.json is incorrect, the latest version ${ + versions["latest"]?.version ?? "" + } is used instead` + ); + useLatestVersion = true; + isIncorrectVersion = true; + } } - } else if ( - useLatestVersion || - !(await getVersionInfo( + } + + return { + adjustedVersion, + isFallback, + isIncorrectVersion, + useLatestVersion, + }; +} + +/** + * Resolves (coerces) requested version based on version map data and CDN data availability. + * + * @param requestedVersion Version to resolve + * @param useLatestVersion Flag whether the latest version should be used + * @param framework Framework type + * @param versions Version map for the current framework + * @param versionInfoJsonFetcher Fetcher function to load remote version data + * @param versionLibsFetcher Fetcher function which loads version libs + * @param modelCachePath Path to model data cache on disk + * @returns Object with resolved version and flag whether the requested version is incorrect and whether the resolved version should be stored in resolution cache + * We don't need to store resolution result in cache if test data available + */ +async function resolveRequestedVersion( + requestedVersion: string, + useLatestVersion: boolean, + framework: UI5Framework, + versions: VersionMapJsonType, + versionInfoJsonFetcher: Fetcher, + versionLibsFetcher: Fetcher, + modelCachePath: string | undefined +): Promise<{ + resolvedVersion: string; + isIncorrectVersion: boolean; + remember: boolean; +}> { + let resolvedVersion = requestedVersion; + let isIncorrectVersion = false; + if ( + !useLatestVersion && + (await isVersionResourcesAvailable( versionInfoJsonFetcher, + versionLibsFetcher, modelCachePath, framework, - adjustedVersion + requestedVersion )) ) { - const requestedVersion = adjustedVersion; - // no version information found, try to negotiate the version - if (!versions) { - // retrieve the version mapping (only exists for SAPUI5 so far) - const url = getVersionJsonUrl(framework); - const response = await versionJsonFetcher(url); - if (response.ok) { - versionMap[framework] = (await response.json()) as Record< - string, - { version: string; support: string; lts: boolean } - >; - } else { - isFallback = true; - getLogger().warn( - "Could not read version mapping, fallback to default version", - { - url, - DEFAULT_UI5_VERSION, - } - ); - versionMap[framework] = { - latest: { - version: DEFAULT_UI5_VERSION, - support: "Maintenance", - lts: true, - }, - }; - } - versions = versionMap[framework]; + return { + resolvedVersion, + isIncorrectVersion: false, + remember: false, + }; + } + + // coerce the version (check for invalid version, which indicates development scenario) + let parsedVersion: semver.SemVer | null = null; + if (!useLatestVersion) { + parsedVersion = semver.coerce(resolvedVersion); + } + + if (!parsedVersion) { + // development scenario or latest version requested (version placeholder in manifest has been found) + resolvedVersion = versions["latest"].version; + isIncorrectVersion = true; + } else { + // lookup for a valid major.minor entry in the versions map + if (versions[`${parsedVersion.major}.${parsedVersion.minor}`]) { + // if entry exists resolve to the latest patch version under maintenance + resolvedVersion = + versions[`${parsedVersion.major}.${parsedVersion.minor}`].version; } - // coerce the version (check for invalid version, which indicates development scenario) - const parsedVersion = semver.coerce(adjustedVersion); - if (!useLatestVersion && parsedVersion) { - if (versions[`${parsedVersion.major}.${parsedVersion.minor}`]) { - // lookup for a valid major.minor entry - adjustedVersion = - versions[`${parsedVersion.major}.${parsedVersion.minor}`].version; - } - if ( - !(await getVersionInfo( - versionInfoJsonFetcher, - modelCachePath, - framework, - adjustedVersion - )) - ) { - // find closest supported version - adjustedVersion = - semverMinSatisfying( - Object.values(versions).map((entry) => { - return entry.version; - }) as string[], - `^${adjustedVersion}` - ) || versions["latest"].version; - isIncorrectVersion = true; - } - } else { - // development scenario or version placeholder in manifest found => use latest version - adjustedVersion = versions["latest"].version; + // check whether version info exists + // we already checked the requested version at the beginning, no need to repeat the request + const versionInfo = + requestedVersion === resolvedVersion + ? undefined + : await getVersionInfo( + versionInfoJsonFetcher, + modelCachePath, + framework, + resolvedVersion + ); + + if (!versionInfo) { + // if no version info found then find closest supported version + const availableVersions = Object.values(versions) + .filter((entry) => !entry.version.startsWith("1.38.")) + .map((entry) => entry.version); + resolvedVersion = + semverMinSatisfying(availableVersions, `^${resolvedVersion}`) || + versions["latest"].version; isIncorrectVersion = true; } - // store the resolved version - if (requestedVersion) { - if (requestedVersion !== adjustedVersion) { - isIncorrectVersion = true; - } - if (!resolvedVersions[framework]) { - resolvedVersions[framework] = Object.create(null); - } - resolvedVersions[framework][requestedVersion] = adjustedVersion; - } } + return { - version: adjustedVersion, - isFallback, + resolvedVersion, isIncorrectVersion, + remember: true, }; } -// Versions 1.38 and older are not supported because of missing features in the API, while the LA relies on that API for validation. -// See https://github.com/SAP/ui5-language-assistant/issues/538 for details -function isVersionSupported(version: string | undefined): boolean { - const versionDetails = semver.coerce(version); - if (!versionDetails) { - return false; +/** + * Returns version map for the given framework + * + * @param framework The UI5 framework version + * @param versionJsonFetcher Fetcher function which loads version json + * @returns Object with versions json + */ +export async function getVersionsMap( + framework: UI5Framework, + versionJsonFetcher: Fetcher +): Promise { + let versions = versionMap[framework]; + if (versions) { + return versions; } - return !( - versionDetails.major === 1 && - versionDetails.minor && - versionDetails.minor <= 38 - ); + + // no version information found in the map, retrieve the version mapping using fetcher + const url = getVersionJsonUrl(framework); + const response = await versionJsonFetcher(url); + if (response.ok) { + versionMap[framework] = await response.json(); + } else { + getLogger().error( + "Could not read version mapping, fallback to default version", + { + url, + DEFAULT_UI5_VERSION, + } + ); + // update version map with default version only + versionMap[framework] = { + latest: { + version: DEFAULT_UI5_VERSION, + support: "Maintenance", + lts: true, + }, + }; + } + versions = versionMap[framework]; + + return versions; } diff --git a/packages/context/test/unit/ui5-model.test.ts b/packages/context/test/unit/ui5-model.test.ts index 5276b5d07..b04f691a7 100644 --- a/packages/context/test/unit/ui5-model.test.ts +++ b/packages/context/test/unit/ui5-model.test.ts @@ -1,5 +1,26 @@ +// mock to test offline mode version negotiation, needed to override original methods +jest.mock("@ui5-language-assistant/logic-utils", () => { + const actual = jest.requireActual("@ui5-language-assistant/logic-utils"); + return { + ...actual, + getLocalUrl: jest.fn().mockImplementation(actual.getLocalUrl), + tryFetch: jest.fn().mockImplementation(actual.tryFetch), + }; +}); + +jest.mock("fs-extra", () => { + const actual = jest.requireActual("fs-extra"); + return { + ...actual, + readJson: jest.fn().mockImplementation(actual.readJson), + pathExists: jest.fn().mockImplementation(actual.pathExists), + lstat: jest.fn().mockImplementation(actual.lstat), + }; +}); + import { dir as tempDir, file as tempFile } from "tmp-promise"; import { readdir, mkdirs, writeFile } from "fs-extra"; +import * as fsExtra from "fs-extra"; import { sync as rimrafSync } from "rimraf"; import { forEach, isPlainObject } from "lodash"; @@ -11,20 +32,72 @@ import { getCacheFilePath, getCacheFolder, negotiateVersionWithFetcher, + VersionMapJsonType, } from "../../src/ui5-model"; import { FetchResponse } from "@ui5-language-assistant/language-server"; +import { fetch } from "@ui5-language-assistant/logic-utils"; +import * as logicUtils from "@ui5-language-assistant/logic-utils"; +import { getVersionJsonUrl } from "../../src/utils"; +import semverMinSatisfying from "semver/ranges/min-satisfying"; +import { Response } from "node-fetch"; + +const GET_MODEL_TIMEOUT = 30000; +const FRAMEWORK = "SAPUI5"; +const OPEN_FRAMEWORK = "OpenUI5"; +const FALLBACK_VERSION = "1.71.61"; +const FALLBACK_VERSION_BASE = "1.71"; +const UI5_VERSION_S4_PLACEHOLDER = "${sap.ui5.dist.version}"; +const NO_CACHE_FOLDER = undefined; + +type UI5FrameworkType = typeof FRAMEWORK | typeof OPEN_FRAMEWORK; +const frameworks = [FRAMEWORK, OPEN_FRAMEWORK] as UI5FrameworkType[]; + +async function getCurrentVersionMaps( + framework: UI5FrameworkType +): Promise { + const url = getVersionJsonUrl(framework); + const response = await fetch(url); + if (response.ok) { + return (await response.json()) as VersionMapJsonType; + } else { + return undefined; + } +} + +const latestFallbackPatchVersion: Record = + { + OpenUI5: undefined, + SAPUI5: undefined, + }; + +const currentVersionMaps: Record = { + OpenUI5: {}, + SAPUI5: {}, +}; + +const loadCurrentVersionMaps = Promise.all([ + getCurrentVersionMaps(FRAMEWORK).then((data) => { + currentVersionMaps.SAPUI5 = data || {}; + latestFallbackPatchVersion.SAPUI5 = data?.[FALLBACK_VERSION_BASE]?.version; + }), + getCurrentVersionMaps(OPEN_FRAMEWORK).then((data) => { + currentVersionMaps.OpenUI5 = data || {}; + latestFallbackPatchVersion.OpenUI5 = data?.[FALLBACK_VERSION_BASE]?.version; + }), +]); describe("the UI5 language assistant ui5 model", () => { // The default timeout is 2000ms and getSemanticModel can take ~3000-5000ms - const GET_MODEL_TIMEOUT = 30000; - const FRAMEWORK = "SAPUI5"; - const OPEN_FRAMEWORK = "OpenUI5"; - const DEFAULT_UI5_VERSION = "1.71.61"; - const UI5_VERSION_S4_PLACEHOLDER = "${sap.ui5.dist.version}"; - const NO_CACHE_FOLDER = undefined; - function assertSemanticModel(ui5Model: UI5SemanticModel): void { - expect(ui5Model.version).toEqual(DEFAULT_UI5_VERSION); + beforeAll(async () => { + await loadCurrentVersionMaps; + }); + + function assertSemanticModel( + ui5Model: UI5SemanticModel, + expectedVersion?: string + ): void { + expect(ui5Model.version).toEqual(expectedVersion || FALLBACK_VERSION); expect(Object.keys(ui5Model.classes).length).toBeGreaterThan(200); expect(Object.keys(ui5Model.namespaces).length).toBeGreaterThan(200); @@ -48,16 +121,23 @@ describe("the UI5 language assistant ui5 model", () => { ); } + it("check loaded version maps correctness", () => { + expect(latestFallbackPatchVersion.SAPUI5?.startsWith("1.71.")).toBeTrue(); + expect(currentVersionMaps.SAPUI5?.["latest"]).toBeDefined(); + expect(latestFallbackPatchVersion.OpenUI5?.startsWith("1.71.")).toBeTrue(); + expect(currentVersionMaps.OpenUI5?.["latest"]).toBeDefined(); + }); + it( "will get UI5 semantic model", async () => { const ui5Model = await getSemanticModel( NO_CACHE_FOLDER, FRAMEWORK, - undefined, + latestFallbackPatchVersion.SAPUI5, true ); - assertSemanticModel(ui5Model); + assertSemanticModel(ui5Model, latestFallbackPatchVersion.SAPUI5); }, GET_MODEL_TIMEOUT ); @@ -100,10 +180,10 @@ describe("the UI5 language assistant ui5 model", () => { const ui5Model = await getSemanticModel( cachePath, FRAMEWORK, - undefined, + latestFallbackPatchVersion.SAPUI5, true ); - assertSemanticModel(ui5Model); + assertSemanticModel(ui5Model, latestFallbackPatchVersion.SAPUI5); // Check the files were created in the folder const files = await readdir(cachePath); @@ -120,7 +200,7 @@ describe("the UI5 language assistant ui5 model", () => { }, cachePath, FRAMEWORK, - undefined, + latestFallbackPatchVersion.SAPUI5, true ); expect(fetcherCalled).toBeFalse(); @@ -141,7 +221,10 @@ describe("the UI5 language assistant ui5 model", () => { ); } }); - assertSemanticModel(ui5ModelFromCache); + assertSemanticModel( + ui5ModelFromCache, + latestFallbackPatchVersion.SAPUI5 + ); }, GET_MODEL_TIMEOUT ); @@ -151,7 +234,7 @@ describe("the UI5 language assistant ui5 model", () => { async () => { // Create a folder with the file name so the file will not be written const cacheFilePath = getCacheFilePath( - getCacheFolder(cachePath, FRAMEWORK, DEFAULT_UI5_VERSION), + getCacheFolder(cachePath, FRAMEWORK, FALLBACK_VERSION), "sap.m" ); expectExists(cacheFilePath, "cacheFilePath"); @@ -160,7 +243,7 @@ describe("the UI5 language assistant ui5 model", () => { const ui5Model = await getSemanticModel( cachePath, FRAMEWORK, - undefined + latestFallbackPatchVersion.SAPUI5 ); expect(ui5Model).not.toBeUndefined(); // Check we still got the sap.m library data @@ -177,7 +260,7 @@ describe("the UI5 language assistant ui5 model", () => { const cacheFolder = getCacheFolder( cachePath, FRAMEWORK, - DEFAULT_UI5_VERSION + FALLBACK_VERSION ); await mkdirs(cacheFolder); const cacheFilePath = getCacheFilePath(cacheFolder, "sap.m"); @@ -216,10 +299,10 @@ describe("the UI5 language assistant ui5 model", () => { const ui5Model = await getSemanticModel( cachePath, FRAMEWORK, - undefined, + latestFallbackPatchVersion.SAPUI5, true ); - assertSemanticModel(ui5Model); + assertSemanticModel(ui5Model, latestFallbackPatchVersion.SAPUI5); // Call getSemanticModel again with the same path and check it doesn't try to read from the URL let fetcherCalled = false; @@ -236,7 +319,7 @@ describe("the UI5 language assistant ui5 model", () => { }, cachePath, FRAMEWORK, - undefined, + latestFallbackPatchVersion.SAPUI5, true ); expect(fetcherCalled).toBeTrue(); @@ -249,52 +332,6 @@ describe("the UI5 language assistant ui5 model", () => { describe("version negotiation", () => { let cachePath: string; let cleanup: () => Promise; - const versionMap = { - SAPUI5: { - latest: { - version: "1.120.3", - support: "Maintenance", - lts: true, - }, - "1.105": { - version: "1.105.0", - support: "Maintenance", - lts: true, - }, - "1.96": { - version: "1.96.27", - support: "Maintenance", - lts: true, - }, - "1.84": { - version: "1.84.41", - support: "Maintenance", - lts: true, - }, - "1.71": { - version: "1.71.70", - support: "Maintenance", - lts: true, - }, - }, - OpenUI5: { - latest: { - version: "1.106.0", - support: "Maintenance", - lts: true, - }, - "1.106": { - version: "1.106.0", - support: "Maintenance", - lts: true, - }, - "1.71": { - version: "1.71.70", - support: "Maintenance", - lts: true, - }, - }, - }; const versionInfo = { libraries: [ { @@ -302,15 +339,28 @@ describe("the UI5 language assistant ui5 model", () => { }, ], }; - const createResponse = (ok: boolean, status: number, json?: unknown) => { + const createSuccessfulResponse = (json: T) => { return { - ok, - status, - json: async (): Promise => { + ok: true, + status: 200, + json: async (): Promise => { return json; }, }; }; + const createFailedResponse = () => { + return { + ok: false, + status: 404, + json: async (): Promise => { + return undefined as T; + }, + }; + }; + + beforeAll(async () => { + await loadCurrentVersionMaps; + }); beforeEach(async () => { ({ path: cachePath, cleanup } = await tempFile()); @@ -320,312 +370,432 @@ describe("the UI5 language assistant ui5 model", () => { await cleanup(); }); - it("resolve the default version", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(true, 200, versionInfo); - }, - cachePath, - FRAMEWORK, - DEFAULT_UI5_VERSION - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual( - DEFAULT_UI5_VERSION - ); - }); + const getExpectedVersion = ( + framework: typeof FRAMEWORK | typeof OPEN_FRAMEWORK, + requestedVersion: string + ) => { + const versions = currentVersionMaps[framework]; + const versionList = Object.values(versions) + .filter((entry) => !entry.version.startsWith("1.38.")) + .map((entry) => entry.version); + const expectedVersion = + semverMinSatisfying(versionList, `^${requestedVersion}`) || + versions["latest"].version; + return expectedVersion; + }; - it("resolve the default version open UI5", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); - }, - async (): Promise => { - return createResponse(true, 200, versionInfo); - }, - cachePath, - OPEN_FRAMEWORK, - DEFAULT_UI5_VERSION - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual( - DEFAULT_UI5_VERSION - ); - }); + describe.each(frameworks)("for framework %s", (framework) => { + it("resolve the default version", async () => { + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createSuccessfulResponse(versionInfo); + }, + async (): Promise => { + return createSuccessfulResponse({}); + }, + cachePath, + framework, + latestFallbackPatchVersion[framework] + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + latestFallbackPatchVersion[framework] + ); + }); - it("resolve available concrete version OpenUI5 (1.106.0)", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); - }, - async (): Promise => { - return createResponse(true, 200, versionInfo); - }, - cachePath, - OPEN_FRAMEWORK, - "1.106.0" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.106.0"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); - }); + it("resolve available concrete version", async () => { + const testVersionKey = Object.keys(currentVersionMaps[framework])[2]; + const testVersion = + currentVersionMaps[framework][testVersionKey].version; + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createSuccessfulResponse(versionInfo); + }, + async (): Promise => { + return createSuccessfulResponse({}); + }, + cachePath, + framework, + testVersion + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual(testVersion); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); + }); - it("resolve available concrete version (1.105.0)", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(true, 200, versionInfo); - }, - cachePath, - FRAMEWORK, - "1.105.0" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.105.0"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); - }); + it("resolve outdated but still available concrete version (1.104.0)", async () => { + const testVersion = "1.104.0"; + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createSuccessfulResponse(versionInfo); + }, + async (): Promise => { + return createSuccessfulResponse({}); + }, + cachePath, + framework, + testVersion + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual(testVersion); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); + }); - it("resolve available concrete version (1.104.0)", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(true, 200, versionInfo); - }, - cachePath, - FRAMEWORK, - "1.104.0" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.104.0"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); - }); + it("resolve outdated but still available in cache concrete version (1.104.0)", async () => { + const testVersion = "1.104.0"; - it("resolve not available concrete version (should be latest)", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - FRAMEWORK, - "1.104.0" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.105.0"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); - }); + // Mock cache reader + const fileReaderSpy = jest + .spyOn(fsExtra, "readJson") + .mockImplementation(async () => { + return { key1: {} }; + }); + const pathExistsSpy = jest + .spyOn(fsExtra, "pathExists") + .mockImplementationOnce(async () => false) // version info json cache read fail, should send request + .mockImplementationOnce(async () => true); // version libs cache read + const lstatSpy = jest + .spyOn(fsExtra, "lstat") + .mockImplementation( + async () => ({ isFile: () => true } as unknown as fsExtra.Stats) + ); + jest.clearAllMocks(); // to reset call counters which are already above zero due to mocked fs-extra module + + try { + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createSuccessfulResponse(versionInfo); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + framework, + testVersion + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual(testVersion); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect( + objNegotiatedVersionWithFetcher.isIncorrectVersion + ).toBeFalse(); + expect(fileReaderSpy).toHaveBeenCalledOnce(); + const arg = fileReaderSpy.mock.calls[0]; + expect( + arg[0] + .replace(/\\/g, "/") + .endsWith( + `ui5-resources-cache/${framework}/1.104.0/sap.ui.core.json` + ) + ).toBeTrue(); + } finally { + fileReaderSpy.mockRestore(); + pathExistsSpy.mockRestore(); + lstatSpy.mockRestore(); + } + }); - it("resolve not available concrete version OpenUI5 (should be latest)", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - OPEN_FRAMEWORK, - "1.104.0" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.106.0"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); + it("resolve outdated version with broken cache data (1.104.0)", async () => { + const testVersion = "1.104.0"; + const expectedVersion = getExpectedVersion(framework, testVersion); + + // Mock cache reader + const fileReaderSpy = jest + .spyOn(fsExtra, "readJson") + .mockImplementation(async () => { + return { key1: {} }; + }); + const pathExistsSpy = jest + .spyOn(fsExtra, "pathExists") + .mockImplementationOnce(async () => false); + + try { + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createSuccessfulResponse(versionInfo); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + framework, + testVersion + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + expectedVersion + ); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); + } finally { + fileReaderSpy.mockRestore(); + pathExistsSpy.mockRestore(); + } + }); + + it("resolve not available concrete version (should be closest)", async () => { + const testVersion = "1.104.0"; + const expectedVersion = getExpectedVersion(framework, testVersion); + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createSuccessfulResponse(versionInfo); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + framework, + testVersion + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + expectedVersion + ); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); + }); }); - it("resolve major.minor versions (should be closest)", async () => { - let objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - FRAMEWORK, - "1.103" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.105.0"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); - objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - FRAMEWORK, - "1.96" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.96.27"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); - objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - FRAMEWORK, - "1.84" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.84.41"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); - objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); + describe("resolve major.minor versions", () => { + const testCases: { + version: () => string; + expected: () => string; + }[] = [ + { + // outdated version + version: () => "1.104", + expected: () => getExpectedVersion(FRAMEWORK, "1.104"), }, - async (): Promise => { - return createResponse(false, 404); + { + // supported version + version: () => Object.keys(currentVersionMaps.SAPUI5)[1], + expected: () => + currentVersionMaps.SAPUI5[Object.keys(currentVersionMaps.SAPUI5)[1]] + .version, }, - cachePath, - FRAMEWORK, - "1.71" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.70"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); - objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); + { + // fallback base + version: () => FALLBACK_VERSION_BASE, + expected: () => latestFallbackPatchVersion.SAPUI5 || "", }, - async (): Promise => { - return createResponse(false, 404); + { + // out of support + version: () => "1.18", + expected: () => latestFallbackPatchVersion.SAPUI5 || "", }, - cachePath, - FRAMEWORK, - "1.18" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.70"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); + ]; + + beforeAll(async () => { + await loadCurrentVersionMaps; + }); + + it.each(testCases)("test case %#", async (testCase) => { + const result = await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps.SAPUI5); + }, + async (): Promise => { + return createFailedResponse(); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + FRAMEWORK, + testCase.version() + ); + expect(result.version).toEqual(testCase.expected()); + expect(result.isFallback).toBeFalse(); + expect(result.isIncorrectVersion).toBeTrue(); + }); }); - it("resolve major version (should be closest)", async () => { + it("resolve major version (should be closest supported)", async () => { + const expectedVersion = getExpectedVersion(FRAMEWORK, "1"); const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps.SAPUI5); + }, async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); + return createFailedResponse(); }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, cachePath, FRAMEWORK, "1" ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.70"); + expect(objNegotiatedVersionWithFetcher.version).toEqual(expectedVersion); expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); }); - it("resolve invalid versions (should be fallback)", async () => { - let objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - FRAMEWORK, - "" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.61"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeTrue(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); - objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[FRAMEWORK]); - }, - async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - FRAMEWORK, - undefined - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.61"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeTrue(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); - }); + it.each(frameworks)( + "resolve invalid versions (should be fallback) %s", + async (framework) => { + let objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createFailedResponse(); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + framework, + "" + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + latestFallbackPatchVersion[framework] + ); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeTrue(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); + + objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps[framework]); + }, + async (): Promise => { + return createFailedResponse(); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + framework, + undefined + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + latestFallbackPatchVersion[framework] + ); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeTrue(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); + } + ); - it("resolve invalid versions OpenUI5 (should be fallback)", async () => { + it("resolve unsupported versions (should be latest fallback)", async () => { const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps.SAPUI5); }, async (): Promise => { - return createResponse(false, 404); - }, - cachePath, - OPEN_FRAMEWORK, - "" - ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.61"); - expect(objNegotiatedVersionWithFetcher.isFallback).toBeTrue(); - expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); - }); - - it("resolve unsupported versions (should be latest)", async () => { - const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( - async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); + return createFailedResponse(); }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, cachePath, FRAMEWORK, "1.38" ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.70"); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + latestFallbackPatchVersion.SAPUI5 + ); expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); }); it("resolve wrong versions (should be latest)", async () => { const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps.SAPUI5); + }, async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); + return createFailedResponse(); }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, cachePath, FRAMEWORK, "a.b" ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.71.70"); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + currentVersionMaps.SAPUI5["latest"].version + ); expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); }); it("resolve unsupported version placeholder - S/4 generator artifact (should be latest)", async () => { const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps.SAPUI5); + }, async (): Promise => { - return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); + return createFailedResponse(); }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, cachePath, FRAMEWORK, UI5_VERSION_S4_PLACEHOLDER ); - expect(objNegotiatedVersionWithFetcher.version).toEqual("1.120.3"); + expect(objNegotiatedVersionWithFetcher.version).toEqual( + currentVersionMaps.SAPUI5["latest"].version + ); expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); }); + + it("offline mode (should be the same as requested)", async () => { + const fetchSpy = jest + .spyOn(logicUtils, "tryFetch") + .mockImplementation(async () => { + return {} as Response; + }); + const localUrlSpy = jest + .spyOn(logicUtils, "getLocalUrl") + .mockReturnValueOnce("localhost"); + + const testVersion = "1.71.1"; + try { + const objNegotiatedVersionWithFetcher = + await negotiateVersionWithFetcher( + async (): Promise> => { + return createSuccessfulResponse(currentVersionMaps.SAPUI5); + }, + async (): Promise => { + return createFailedResponse(); + }, + async (): Promise => { + return createFailedResponse(); + }, + cachePath, + FRAMEWORK, + testVersion + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual(testVersion); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeFalse(); + } finally { + localUrlSpy.mockRestore(); + fetchSpy.mockRestore(); + } + }); }); }); diff --git a/packages/context/test/unit/ui5-model_fs-mocked.test.ts b/packages/context/test/unit/ui5-model_fs-mocked.test.ts index e330f54a3..daf6d3dd2 100644 --- a/packages/context/test/unit/ui5-model_fs-mocked.test.ts +++ b/packages/context/test/unit/ui5-model_fs-mocked.test.ts @@ -8,7 +8,10 @@ jest.mock("fs-extra", () => { import { file as tempFile } from "tmp-promise"; -import { negotiateVersionWithFetcher } from "../../src/ui5-model"; +import { + VersionMapJsonType, + negotiateVersionWithFetcher, +} from "../../src/ui5-model"; import { FetchResponse } from "@ui5-language-assistant/language-server"; import { DEFAULT_UI5_VERSION } from "../../src/types"; @@ -19,12 +22,12 @@ describe("the UI5 language assistant ui5 model", () => { describe("version negotiation", () => { let cachePath: string; let cleanup: () => Promise; - const createResponse = (ok: boolean, status: number, json?: unknown) => { + const createFailedResponse = () => { return { - ok, - status, - json: async (): Promise => { - return json; + ok: false, + status: 404, + json: async (): Promise => { + return {} as T; }, }; }; @@ -39,11 +42,14 @@ describe("the UI5 language assistant ui5 model", () => { it("fallback to default (while versions map is empty)", async () => { const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise> => { + return createFailedResponse(); + }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, cachePath, FRAMEWORK, @@ -59,11 +65,14 @@ describe("the UI5 language assistant ui5 model", () => { // checking resolution from the cache to cover this case const objNegotiatedVersionWithFetcher2 = await negotiateVersionWithFetcher( + async (): Promise> => { + return createFailedResponse(); + }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, async (): Promise => { - return createResponse(false, 404); + return createFailedResponse(); }, cachePath, FRAMEWORK, diff --git a/packages/language-server/api.d.ts b/packages/language-server/api.d.ts index 87ec846c9..881647d76 100644 --- a/packages/language-server/api.d.ts +++ b/packages/language-server/api.d.ts @@ -25,10 +25,10 @@ export type ServerInitializationOptions = { logLevel?: LogLevel; }; -export type FetchResponse = { +export type FetchResponse = { ok: boolean; status: number; - json: () => Promise; + json: () => Promise; }; export type Fetcher = (url: string) => Promise; diff --git a/packages/language-server/test/unit/completion-items-classes.test.ts b/packages/language-server/test/unit/completion-items-classes.test.ts index 358806dac..be0739ece 100644 --- a/packages/language-server/test/unit/completion-items-classes.test.ts +++ b/packages/language-server/test/unit/completion-items-classes.test.ts @@ -8,6 +8,8 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generateModel, expectExists, + getFallbackPatchVersions, + DEFAULT_UI5_VERSION, } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { @@ -26,7 +28,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () => beforeAll(async function () { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/language-server/test/unit/completion-items-literals.test.ts b/packages/language-server/test/unit/completion-items-literals.test.ts index c975e8894..c52de2d9e 100644 --- a/packages/language-server/test/unit/completion-items-literals.test.ts +++ b/packages/language-server/test/unit/completion-items-literals.test.ts @@ -1,7 +1,11 @@ import { map, uniq } from "lodash"; import { CompletionItemKind, TextEdit } from "vscode-languageserver"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { getDefaultContext, @@ -17,7 +21,9 @@ describe("the UI5 language assistant Code Completion Services", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/language-server/test/unit/completion-items.test.ts b/packages/language-server/test/unit/completion-items.test.ts index 0bd14b47a..dba63a345 100644 --- a/packages/language-server/test/unit/completion-items.test.ts +++ b/packages/language-server/test/unit/completion-items.test.ts @@ -3,7 +3,11 @@ import { CompletionItemKind, TextEdit } from "vscode-languageserver"; import { UI5XMLViewCompletion } from "@ui5-language-assistant/xml-views-completion"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { Settings } from "@ui5-language-assistant/settings"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { computeLSPKind } from "../../src/completion-items"; import { @@ -30,7 +34,9 @@ describe("the UI5 language assistant Code Completion Services", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/language-server/test/unit/documentation.test.ts b/packages/language-server/test/unit/documentation.test.ts index e7ecf5998..a3c81df0e 100644 --- a/packages/language-server/test/unit/documentation.test.ts +++ b/packages/language-server/test/unit/documentation.test.ts @@ -1,6 +1,8 @@ import { + DEFAULT_UI5_VERSION, buildUI5Enum, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; @@ -11,7 +13,9 @@ describe("The @ui5-language-assistant/language-server fun beforeAll(async function () { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); }); diff --git a/packages/language-server/test/unit/hover.test.ts b/packages/language-server/test/unit/hover.test.ts index 198ae6cc5..6e9636cce 100644 --- a/packages/language-server/test/unit/hover.test.ts +++ b/packages/language-server/test/unit/hover.test.ts @@ -10,6 +10,8 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generateModel, expectExists, + getFallbackPatchVersions, + DEFAULT_UI5_VERSION, } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { getHoverResponse } from "../../src/hover"; @@ -23,7 +25,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/language-server/test/unit/quick-fix.test.ts b/packages/language-server/test/unit/quick-fix.test.ts index 93bf8c3c0..2f745bee2 100644 --- a/packages/language-server/test/unit/quick-fix.test.ts +++ b/packages/language-server/test/unit/quick-fix.test.ts @@ -1,4 +1,8 @@ -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { Position, Range } from "vscode-languageserver-types"; import { generate } from "@ui5-language-assistant/semantic-model"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; @@ -20,7 +24,9 @@ describe("The @ui5-language-assistant/language-server diagnostics quick fix func beforeAll(async function () { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/language-server/test/unit/snapshots/xml-view-diagnostics/snapshots-utils.ts b/packages/language-server/test/unit/snapshots/xml-view-diagnostics/snapshots-utils.ts index 304f122bb..56c116a97 100644 --- a/packages/language-server/test/unit/snapshots/xml-view-diagnostics/snapshots-utils.ts +++ b/packages/language-server/test/unit/snapshots/xml-view-diagnostics/snapshots-utils.ts @@ -4,7 +4,11 @@ import { TextDocument } from "vscode-languageserver"; import { readJsonSync, readFileSync } from "fs-extra"; import { expect } from "chai"; import { sortBy, forEachRight, map, cloneDeep, forEach } from "lodash"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generate } from "@ui5-language-assistant/semantic-model"; import { getXMLViewDiagnostics } from "../../../../src/xml-view-diagnostics"; @@ -94,11 +98,20 @@ export function readSnapshotDiagnosticsLSPResponse( return expectedDiagnostics; } -const ui5ModelPromise = generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, +const ui5ModelPromise = new Promise((done, reject) => { + getFallbackPatchVersions() + .then((patch) => { + generateModel({ + framework: "SAPUI5", + version: patch.SAPUI5 as typeof DEFAULT_UI5_VERSION, + modelGenerator: generate, + }) + .then((model) => done(model)) + .catch((e) => reject(e)); + }) + .catch((e) => reject(e)); }); + let ui5Model: UI5SemanticModel | undefined = undefined; let appContext: AppContext; diff --git a/packages/logic-utils/src/utils/types.ts b/packages/logic-utils/src/utils/types.ts index ba5996fa1..0e35c21fe 100644 --- a/packages/logic-utils/src/utils/types.ts +++ b/packages/logic-utils/src/utils/types.ts @@ -1,5 +1,5 @@ -export type FetchResponse = { +export type FetchResponse = { ok: boolean; status: number; - json: () => Promise; + json: () => Promise; }; diff --git a/packages/logic-utils/test/unit/find-classes-matching.test.ts b/packages/logic-utils/test/unit/find-classes-matching.test.ts index 1842d233d..eaa8e6033 100644 --- a/packages/logic-utils/test/unit/find-classes-matching.test.ts +++ b/packages/logic-utils/test/unit/find-classes-matching.test.ts @@ -1,7 +1,11 @@ import { map, forEach, cloneDeep } from "lodash"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { findClassesMatchingType, @@ -9,14 +13,21 @@ import { classIsOfType, } from "../../src/api"; +async function generateModelForLatestPatch(): Promise { + const { SAPUI5: latestPatchVersion } = await getFallbackPatchVersions(); + return await generateModel({ + framework: "SAPUI5", + version: latestPatchVersion as typeof DEFAULT_UI5_VERSION, + modelGenerator: generate, + }); +} + +const modelGeneratorPromise = generateModelForLatestPatch(); + describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("can locate classes matching an interface directly", () => { @@ -71,14 +82,8 @@ describe("The @ui5-language-assistant/logic-utils func describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; - const beforeAllPromise = generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); - beforeAll(async () => { - ui5Model = await beforeAllPromise; + ui5Model = await modelGeneratorPromise; }); it("can tell if the class matching a UI5Interface type", () => { @@ -102,7 +107,7 @@ describe("The @ui5-language-assistant/logic-utils function", () describe("classes with returnTypes specified in metadata", () => { let adaptedUI5Model: UI5SemanticModel; beforeAll(async () => { - adaptedUI5Model = cloneDeep(await beforeAllPromise); + adaptedUI5Model = cloneDeep(await modelGeneratorPromise); adaptedUI5Model.classes["sap.ui.layout.BlockLayout"].returnTypes = [ adaptedUI5Model.classes["sap.ui.layout.form.FormElement"], ]; diff --git a/packages/logic-utils/test/unit/get-super-class.test.ts b/packages/logic-utils/test/unit/get-super-class.test.ts index 1ec07a933..3de78fa99 100644 --- a/packages/logic-utils/test/unit/get-super-class.test.ts +++ b/packages/logic-utils/test/unit/get-super-class.test.ts @@ -2,8 +2,10 @@ import { cloneDeep } from "lodash"; import { UI5Class } from "@ui5-language-assistant/semantic-model-types"; import { generate } from "@ui5-language-assistant/semantic-model"; import { + DEFAULT_UI5_VERSION, buildUI5Class, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { getSuperClasses } from "../../src/api"; @@ -36,7 +38,9 @@ describe("The @ui5-language-assistant/logic-utils function", ( const ui5Model = cloneDeep( await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }) ); diff --git a/packages/logic-utils/test/unit/xml-node-to-ui5-node.test.ts b/packages/logic-utils/test/unit/xml-node-to-ui5-node.test.ts index 17acce924..cf3271ea1 100644 --- a/packages/logic-utils/test/unit/xml-node-to-ui5-node.test.ts +++ b/packages/logic-utils/test/unit/xml-node-to-ui5-node.test.ts @@ -6,6 +6,8 @@ import { generate } from "@ui5-language-assistant/semantic-model"; import { generateModel, expectExists, + getFallbackPatchVersions, + DEFAULT_UI5_VERSION, } from "@ui5-language-assistant/test-utils"; import { getUI5ClassByXMLElement, @@ -17,14 +19,21 @@ import { getUI5NodeByXMLAttribute, } from "../../src/api"; +async function generateModelForLatestPatch(): Promise { + const { SAPUI5: latestPatchVersion } = await getFallbackPatchVersions(); + return await generateModel({ + framework: "SAPUI5", + version: latestPatchVersion as typeof DEFAULT_UI5_VERSION, + modelGenerator: generate, + }); +} + +const modelGeneratorPromise = generateModelForLatestPatch(); + describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("returns the class for class in the default namespace", () => { @@ -103,11 +112,7 @@ describe("The @ui5-language-assistant/logic-utils func describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("returns the class for class in the default namespace", () => { @@ -185,11 +190,7 @@ describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("returns the aggregation for known aggregation under a class tag", () => { @@ -335,11 +336,7 @@ describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("returns undefined for unknown class", () => { @@ -430,11 +427,7 @@ describe("The @ui5-language-assistant/logic-utils fun describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("returns undefined for unknown class", () => { @@ -494,11 +487,7 @@ describe("The @ui5-language-assistant/logic-utils function", () => { let ui5Model: UI5SemanticModel; beforeAll(async () => { - ui5Model = await generateModel({ - framework: "SAPUI5", - version: "1.71.61", - modelGenerator: generate, - }); + ui5Model = await modelGeneratorPromise; }); it("returns the namespace for tag in a defined namespace", () => { diff --git a/packages/semantic-model/test/unit/api-negative.test.ts b/packages/semantic-model/test/unit/api-negative.test.ts index e142d1898..64a02c305 100644 --- a/packages/semantic-model/test/unit/api-negative.test.ts +++ b/packages/semantic-model/test/unit/api-negative.test.ts @@ -1,5 +1,8 @@ import { keys } from "lodash"; -import { expectExists } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + expectExists, +} from "@ui5-language-assistant/test-utils"; import { UI5SemanticModel, UnresolvedType, @@ -7,7 +10,6 @@ import { import { forEachSymbol } from "../../src/utils"; import { generate } from "../../src/api"; -const DEFAULT_UI5_VERSION = "1.71.61"; describe("The ui5-language-assistant semantic model package API negative tests", () => { describe("generate from model with invalid library file", () => { const message = "not a valid library file"; diff --git a/packages/semantic-model/test/unit/api.test.ts b/packages/semantic-model/test/unit/api.test.ts index 7f064ea9d..a876abb9d 100644 --- a/packages/semantic-model/test/unit/api.test.ts +++ b/packages/semantic-model/test/unit/api.test.ts @@ -5,6 +5,7 @@ import { expectProperty, expectExists, downloadLibraries, + DEFAULT_UI5_VERSION, } from "@ui5-language-assistant/test-utils"; import { UI5Framework, @@ -306,7 +307,12 @@ describe("The ui5-language-assistant semantic model package API", () => { } // TODO: old patches may be removed, should be updated continuously - const versions: TestModelVersion[] = ["1.71.61", "1.84.41", "1.96.27"]; + const versions: TestModelVersion[] = [ + DEFAULT_UI5_VERSION, + "1.84.41", + "1.96.27", + // "1.105.0", + ]; for (const version of versions) { // TODO: consider also openui5? createModelConsistencyTests("SAPUI5", version); @@ -319,7 +325,7 @@ describe("The ui5-language-assistant semantic model package API", () => { beforeAll(async () => { model = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: DEFAULT_UI5_VERSION, modelGenerator: generate, }); }); @@ -388,7 +394,7 @@ describe("The ui5-language-assistant semantic model package API", () => { beforeAll(async () => { model = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: DEFAULT_UI5_VERSION, modelGenerator: generate, }); }); diff --git a/packages/semantic-model/test/unit/unit.test.ts b/packages/semantic-model/test/unit/unit.test.ts index 66b1f537a..2a463d1e9 100644 --- a/packages/semantic-model/test/unit/unit.test.ts +++ b/packages/semantic-model/test/unit/unit.test.ts @@ -3,6 +3,7 @@ import { buildUI5Model, buildUI5Class, expectExists, + DEFAULT_UI5_VERSION, } from "@ui5-language-assistant/test-utils"; import { UI5Class, @@ -70,7 +71,7 @@ describe("The ui5-language-assistant semantic model package unit tests", () => { describe("generate", () => { function generateSymbol(symbol: SymbolBase): UI5SemanticModel { return generate({ - version: "1.71.61", + version: DEFAULT_UI5_VERSION, strict: true, typeNameFix: {}, libraries: { @@ -210,7 +211,7 @@ describe("The ui5-language-assistant semantic model package unit tests", () => { describe("symbols is undefined", () => { const fileContent = { "$schema-ref": "http://schemas.sap.com/sapui5/designtime/api.json/1.0", - version: "1.71.61", + version: DEFAULT_UI5_VERSION, library: "testLib", }; @@ -241,7 +242,7 @@ describe("The ui5-language-assistant semantic model package unit tests", () => { libraries: Record ): UI5SemanticModel { const result = generate({ - version: "1.71.61", + version: DEFAULT_UI5_VERSION, strict: false, typeNameFix: {}, libraries: libraries, diff --git a/packages/semantic-model/test/unit/utils.test.ts b/packages/semantic-model/test/unit/utils.test.ts index bf113fffd..c3d67b681 100644 --- a/packages/semantic-model/test/unit/utils.test.ts +++ b/packages/semantic-model/test/unit/utils.test.ts @@ -1,6 +1,8 @@ import { + DEFAULT_UI5_VERSION, expectExists, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { findSymbol, generate } from "../../src/api"; @@ -12,7 +14,9 @@ describe("The semantic model utils", () => { beforeAll(async () => { model = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); }); diff --git a/packages/vscode-ui5-language-assistant/README.md b/packages/vscode-ui5-language-assistant/README.md index e838399eb..962ebd6b3 100644 --- a/packages/vscode-ui5-language-assistant/README.md +++ b/packages/vscode-ui5-language-assistant/README.md @@ -200,11 +200,9 @@ When configuring local web server, make sure it responds to the exact UI5 versio This extension derives the UI5 version in the following sequence: -1. The `minUI5Version` from the manifest.json file (see note) -2. Lookup in CDN for UI5 version and negotiate to the closest LTS version (see note). -3. If it is not found or the version is 1.38 or older, then default back to 1.71 (latest patch level). - -Note: If `minUI5Version` not found in the manifest.json or lookup in CDN fails, then fall back to default 1.71.60 version +1. If the exact version defined in the `minUI5Version` setting is not retrievable from local cache or CDN, the closest available LTS version is used. +2. If the version is 1.38 or older, or if minUI5Version is not defined, fall back to 1.71 (latest patch level) is applied. +3. If `minUI5Version` is provided in incorrect format, the latest patch version available in CDN is used. The framework(SAPUI5/OpenUI5) is derived from the ui5.yaml file. This defaults to SAPUI5. diff --git a/packages/xml-views-completion/test/unit/api.test.ts b/packages/xml-views-completion/test/unit/api.test.ts index 2857167e5..0ee973877 100644 --- a/packages/xml-views-completion/test/unit/api.test.ts +++ b/packages/xml-views-completion/test/unit/api.test.ts @@ -5,7 +5,11 @@ import { UI5Aggregation, UI5SemanticModel, } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { CodeAssistSettings } from "@ui5-language-assistant/settings"; import { ui5NodeToFQN } from "@ui5-language-assistant/logic-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; @@ -23,7 +27,9 @@ describe("The `getXMLViewCompletions()` api", () => { beforeAll(async function () { REAL_UI5_MODEL = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(REAL_UI5_MODEL); diff --git a/packages/xml-views-completion/test/unit/providers/attributeName/namespace.test.ts b/packages/xml-views-completion/test/unit/providers/attributeName/namespace.test.ts index d173a6b44..59b2061fa 100644 --- a/packages/xml-views-completion/test/unit/providers/attributeName/namespace.test.ts +++ b/packages/xml-views-completion/test/unit/providers/attributeName/namespace.test.ts @@ -12,9 +12,11 @@ import { import { generate } from "@ui5-language-assistant/semantic-model"; import { ui5NodeToFQN } from "@ui5-language-assistant/logic-utils"; import { + DEFAULT_UI5_VERSION, expectSuggestions, expectXMLAttribute, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { isExistingNamespaceAttribute, @@ -154,7 +156,9 @@ describe("The ui5-language-assistant xml-views-completion", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-completion/test/unit/providers/attributeName/prop-event-assoc.test.ts b/packages/xml-views-completion/test/unit/providers/attributeName/prop-event-assoc.test.ts index cdbd88066..8bd930025 100644 --- a/packages/xml-views-completion/test/unit/providers/attributeName/prop-event-assoc.test.ts +++ b/packages/xml-views-completion/test/unit/providers/attributeName/prop-event-assoc.test.ts @@ -3,9 +3,11 @@ import { XMLAttribute } from "@xml-tools/ast"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generate } from "@ui5-language-assistant/semantic-model"; import { + DEFAULT_UI5_VERSION, expectSuggestions, expectXMLAttribute, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { propEventAssocSuggestions } from "../../../../src/providers/attributeName/prop-event-assoc"; import { UI5XMLViewCompletion } from "../../../../api"; @@ -62,7 +64,9 @@ describe("The ui5-language-assistant xml-views-completion", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-completion/test/unit/providers/attributeValue/boolean-literal.test.ts b/packages/xml-views-completion/test/unit/providers/attributeValue/boolean-literal.test.ts index b84461608..311247d8b 100644 --- a/packages/xml-views-completion/test/unit/providers/attributeValue/boolean-literal.test.ts +++ b/packages/xml-views-completion/test/unit/providers/attributeValue/boolean-literal.test.ts @@ -1,7 +1,11 @@ import { forEach, map } from "lodash"; import { XMLAttribute, XMLElement } from "@xml-tools/ast"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { booleanSuggestions } from "../../../../src/providers/attributeValue/boolean-literal"; import { @@ -17,7 +21,9 @@ describe("The ui5-language-assistant xml-views-completion", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-completion/test/unit/providers/attributeValue/enum.test.ts b/packages/xml-views-completion/test/unit/providers/attributeValue/enum.test.ts index 64e8246b4..80063f682 100644 --- a/packages/xml-views-completion/test/unit/providers/attributeValue/enum.test.ts +++ b/packages/xml-views-completion/test/unit/providers/attributeValue/enum.test.ts @@ -1,6 +1,10 @@ import { forEach, map } from "lodash"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { XMLAttribute, XMLElement } from "@xml-tools/ast"; import { enumSuggestions } from "../../../../src/providers/attributeValue/enum"; @@ -14,7 +18,9 @@ describe("The ui5-language-assistant xml-views-completion", () => { beforeAll(async function () { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-completion/test/unit/providers/attributeValue/namespace.test.ts b/packages/xml-views-completion/test/unit/providers/attributeValue/namespace.test.ts index 75b9a411e..de086f856 100644 --- a/packages/xml-views-completion/test/unit/providers/attributeValue/namespace.test.ts +++ b/packages/xml-views-completion/test/unit/providers/attributeValue/namespace.test.ts @@ -4,9 +4,11 @@ import { ui5NodeToFQN } from "@ui5-language-assistant/logic-utils"; import { UI5NamespacesInXMLAttributeValueCompletion } from "@ui5-language-assistant/xml-views-completion"; import { generate } from "@ui5-language-assistant/semantic-model"; import { + DEFAULT_UI5_VERSION, expectSuggestions, expectXMLAttribute, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { namespaceValueSuggestions } from "../../../../src/providers/attributeValue/namespace"; import { @@ -31,7 +33,9 @@ describe("The ui5-editor-tools xml-views-completion", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-completion/test/unit/providers/elementName/aggregation.test.ts b/packages/xml-views-completion/test/unit/providers/elementName/aggregation.test.ts index 333de587a..d25903acf 100644 --- a/packages/xml-views-completion/test/unit/providers/elementName/aggregation.test.ts +++ b/packages/xml-views-completion/test/unit/providers/elementName/aggregation.test.ts @@ -2,8 +2,10 @@ import { map, cloneDeep, forEach } from "lodash"; import { XMLElement } from "@xml-tools/ast"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { + DEFAULT_UI5_VERSION, buildUI5Aggregation, generateModel, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { aggregationSuggestions } from "../../../../src/providers/elementName/aggregation"; @@ -17,7 +19,9 @@ describe("The ui5-language-assistant xml-views-completion", () => { beforeAll(async () => { REAL_UI5_MODEL = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(REAL_UI5_MODEL); diff --git a/packages/xml-views-completion/test/unit/providers/elementName/classes.test.ts b/packages/xml-views-completion/test/unit/providers/elementName/classes.test.ts index 642d3c975..3f5cbf6fc 100644 --- a/packages/xml-views-completion/test/unit/providers/elementName/classes.test.ts +++ b/packages/xml-views-completion/test/unit/providers/elementName/classes.test.ts @@ -5,7 +5,11 @@ import { UI5Class, UI5SemanticModel, } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { getSuperClasses, @@ -25,7 +29,9 @@ describe("The ui5-language-assistant xml-views-completion", () => { beforeAll(async function () { ui5Model = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5Model); diff --git a/packages/xml-views-tooltip/test/unit/tooltip.test.ts b/packages/xml-views-tooltip/test/unit/tooltip.test.ts index 69b449855..281e47e98 100644 --- a/packages/xml-views-tooltip/test/unit/tooltip.test.ts +++ b/packages/xml-views-tooltip/test/unit/tooltip.test.ts @@ -10,6 +10,8 @@ import { generate } from "@ui5-language-assistant/semantic-model"; import { generateModel, expectExists, + DEFAULT_UI5_VERSION, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { findUI5HoverNodeAtOffset } from "../../src/api"; import { Context as AppContext } from "@ui5-language-assistant/context"; @@ -21,7 +23,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/api.test.ts b/packages/xml-views-validation/test/unit/api.test.ts index c9212f02a..0593d9fa5 100644 --- a/packages/xml-views-validation/test/unit/api.test.ts +++ b/packages/xml-views-validation/test/unit/api.test.ts @@ -1,6 +1,10 @@ import { map, cloneDeep } from "lodash"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { DocumentCstNode, parse } from "@xml-tools/parser"; import { buildAst } from "@xml-tools/ast"; @@ -17,7 +21,9 @@ describe("the ui5 xml views validations API", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/attributes/invalid-boolean-value.test.ts b/packages/xml-views-validation/test/unit/validators/attributes/invalid-boolean-value.test.ts index 0493ef701..804102f18 100644 --- a/packages/xml-views-validation/test/unit/validators/attributes/invalid-boolean-value.test.ts +++ b/packages/xml-views-validation/test/unit/validators/attributes/invalid-boolean-value.test.ts @@ -1,5 +1,9 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validators } from "../../../../src/api"; import { @@ -15,7 +19,9 @@ describe("the invalid boolean value validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/attributes/unknown-attribute-key.test.ts b/packages/xml-views-validation/test/unit/validators/attributes/unknown-attribute-key.test.ts index 934154411..fd4a946c5 100644 --- a/packages/xml-views-validation/test/unit/validators/attributes/unknown-attribute-key.test.ts +++ b/packages/xml-views-validation/test/unit/validators/attributes/unknown-attribute-key.test.ts @@ -5,6 +5,8 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generateModel, expectExists, + DEFAULT_UI5_VERSION, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validators } from "../../../../src/api"; @@ -21,7 +23,9 @@ describe("the unknown attribute name validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/attributes/unknown-enum-value.test.ts b/packages/xml-views-validation/test/unit/validators/attributes/unknown-enum-value.test.ts index ca15f36a0..39a6fc7ea 100644 --- a/packages/xml-views-validation/test/unit/validators/attributes/unknown-enum-value.test.ts +++ b/packages/xml-views-validation/test/unit/validators/attributes/unknown-enum-value.test.ts @@ -1,5 +1,9 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validators } from "../../../../src/api"; import { @@ -15,7 +19,9 @@ describe("the unknown enum value validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/attributes/unknown-xmlns-namespace.test.ts b/packages/xml-views-validation/test/unit/validators/attributes/unknown-xmlns-namespace.test.ts index c81f36b52..32f2f575e 100644 --- a/packages/xml-views-validation/test/unit/validators/attributes/unknown-xmlns-namespace.test.ts +++ b/packages/xml-views-validation/test/unit/validators/attributes/unknown-xmlns-namespace.test.ts @@ -1,5 +1,9 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { testValidationsScenario, @@ -15,7 +19,9 @@ describe("the unknown namespace in xmlns attribute value validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/attributes/use-of-depracated-attribute.test.ts b/packages/xml-views-validation/test/unit/validators/attributes/use-of-depracated-attribute.test.ts index dd29c3375..4af49ec2e 100644 --- a/packages/xml-views-validation/test/unit/validators/attributes/use-of-depracated-attribute.test.ts +++ b/packages/xml-views-validation/test/unit/validators/attributes/use-of-depracated-attribute.test.ts @@ -5,6 +5,8 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generateModel, expectExists, + DEFAULT_UI5_VERSION, + getFallbackPatchVersions, } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validators } from "../../../../src/api"; @@ -25,7 +27,9 @@ describe("the use of deprecated attribute validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/document/non-unique-id.test.ts b/packages/xml-views-validation/test/unit/validators/document/non-unique-id.test.ts index 5b5470932..5062f8905 100644 --- a/packages/xml-views-validation/test/unit/validators/document/non-unique-id.test.ts +++ b/packages/xml-views-validation/test/unit/validators/document/non-unique-id.test.ts @@ -1,5 +1,9 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validations, @@ -27,7 +31,9 @@ describe("the use of non unique id validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/element/cardinality-of-aggregation.test.ts b/packages/xml-views-validation/test/unit/validators/element/cardinality-of-aggregation.test.ts index 5e1a739e9..aa579017d 100644 --- a/packages/xml-views-validation/test/unit/validators/element/cardinality-of-aggregation.test.ts +++ b/packages/xml-views-validation/test/unit/validators/element/cardinality-of-aggregation.test.ts @@ -1,6 +1,10 @@ import { partial } from "lodash"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validations, @@ -22,7 +26,9 @@ describe("the cardinality aggregation validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/element/non-stable-id.test.ts b/packages/xml-views-validation/test/unit/validators/element/non-stable-id.test.ts index 652a14b37..8612588b3 100644 --- a/packages/xml-views-validation/test/unit/validators/element/non-stable-id.test.ts +++ b/packages/xml-views-validation/test/unit/validators/element/non-stable-id.test.ts @@ -1,6 +1,10 @@ import { partial } from "lodash"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validations, @@ -22,7 +26,9 @@ describe("the use of non stable id validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/element/type-of-aggregation.test.ts b/packages/xml-views-validation/test/unit/validators/element/type-of-aggregation.test.ts index ba54992e9..849d31793 100644 --- a/packages/xml-views-validation/test/unit/validators/element/type-of-aggregation.test.ts +++ b/packages/xml-views-validation/test/unit/validators/element/type-of-aggregation.test.ts @@ -3,7 +3,11 @@ import { UI5SemanticModel, UI5Aggregation, } from "@ui5-language-assistant/semantic-model-types"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { validations, @@ -25,7 +29,9 @@ describe("the type aggregation validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-aggregation.test.ts b/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-aggregation.test.ts index 7d5d05936..463a1c581 100644 --- a/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-aggregation.test.ts +++ b/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-aggregation.test.ts @@ -1,7 +1,11 @@ import { partial, find } from "lodash"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generate } from "@ui5-language-assistant/semantic-model"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { validators } from "../../../../src/api"; import { buildDeprecatedIssueMessage, @@ -20,7 +24,9 @@ describe("the use of deprecated aggregation validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-class.test.ts b/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-class.test.ts index bba12c41b..4d774a5b7 100644 --- a/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-class.test.ts +++ b/packages/xml-views-validation/test/unit/validators/element/use-of-deprecated-class.test.ts @@ -1,6 +1,10 @@ import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { generate } from "@ui5-language-assistant/semantic-model"; -import { generateModel } from "@ui5-language-assistant/test-utils"; +import { + DEFAULT_UI5_VERSION, + generateModel, + getFallbackPatchVersions, +} from "@ui5-language-assistant/test-utils"; import { validators } from "../../../../src/api"; import { computeExpectedRange, @@ -15,7 +19,9 @@ describe("the use of deprecated class validation", () => { beforeAll(async () => { ui5SemanticModel = await generateModel({ framework: "SAPUI5", - version: "1.71.61", + version: ( + await getFallbackPatchVersions() + ).SAPUI5 as typeof DEFAULT_UI5_VERSION, modelGenerator: generate, }); appContext = getDefaultContext(ui5SemanticModel); diff --git a/test-packages/test-utils/api.d.ts b/test-packages/test-utils/api.d.ts index 12057b0ae..46fb43006 100644 --- a/test-packages/test-utils/api.d.ts +++ b/test-packages/test-utils/api.d.ts @@ -88,9 +88,11 @@ export function buildUI5Model>( opts: Partial ): UI5SemanticModel & Pick; +export const DEFAULT_UI5_VERSION = "1.71.61"; + // TODO: list should be updated continuously! export type TestModelVersion = - | /* OOM */ "1.71.61" + | /* OOM */ typeof DEFAULT_UI5_VERSION | "1.84.41" | "1.96.27" | "1.108.26" @@ -168,3 +170,8 @@ export type generateFunc = (opts: { strict: boolean; printValidationErrors?: boolean; }) => UI5SemanticModel; + +export async function getFallbackPatchVersions(): Promise<{ + SAPUI5: string | undefined; + OpenUI5: string | undefined; +}>; diff --git a/test-packages/test-utils/src/api.ts b/test-packages/test-utils/src/api.ts index fa6f4d6e0..72db103ea 100644 --- a/test-packages/test-utils/src/api.ts +++ b/test-packages/test-utils/src/api.ts @@ -29,3 +29,6 @@ export { expectExists, expectProperty, } from "./utils/expect"; +export { getFallbackPatchVersions } from "./utils/download-ui5-resources"; + +export const DEFAULT_UI5_VERSION = "1.71.61"; diff --git a/test-packages/test-utils/src/utils/download-ui5-resources.ts b/test-packages/test-utils/src/utils/download-ui5-resources.ts index e739cc53d..70d9d9686 100644 --- a/test-packages/test-utils/src/utils/download-ui5-resources.ts +++ b/test-packages/test-utils/src/utils/download-ui5-resources.ts @@ -2,6 +2,11 @@ import { zipObject, map, noop, get } from "lodash"; import { resolve } from "path"; import { writeFile, mkdirs, pathExists } from "fs-extra"; +export const UI5_FRAMEWORK_CDN_BASE_URL = { + OpenUI5: "https://sdk.openui5.org/", + SAPUI5: "https://ui5.sap.com/", +}; + const importDynamic = (modulePath: string) => { try { return import(modulePath); @@ -113,3 +118,42 @@ async function writeUrlToFile(url: string, file: string): Promise { } await writeFile(file, text); } + +type VersionMapJsonType = Record< + string, + { version: string; support: string; lts: boolean } +>; + +const FRAMEWORK = "SAPUI5"; +const OPEN_FRAMEWORK = "OpenUI5"; +const FALLBACK_VERSION_BASE = "1.71"; + +async function getCurrentVersionMaps( + framework: typeof FRAMEWORK | typeof OPEN_FRAMEWORK +): Promise { + const url = `${UI5_FRAMEWORK_CDN_BASE_URL[framework]}version.json`; + const response = await fetch(url); + if (response.ok) { + return (await response.json()) as VersionMapJsonType; + } else { + return undefined; + } +} + +export async function getFallbackPatchVersions(): Promise<{ + SAPUI5: string | undefined; + OpenUI5: string | undefined; +}> { + const result: { SAPUI5: string | undefined; OpenUI5: string | undefined } = { + OpenUI5: undefined, + SAPUI5: undefined, + }; + + result.SAPUI5 = (await getCurrentVersionMaps(FRAMEWORK))?.[ + FALLBACK_VERSION_BASE + ]?.version; + result.OpenUI5 = (await getCurrentVersionMaps(OPEN_FRAMEWORK))?.[ + FALLBACK_VERSION_BASE + ]?.version; + return result; +} diff --git a/test-packages/test-utils/src/utils/semantic-model-provider.ts b/test-packages/test-utils/src/utils/semantic-model-provider.ts index c65a9d8bf..a14b4fc02 100644 --- a/test-packages/test-utils/src/utils/semantic-model-provider.ts +++ b/test-packages/test-utils/src/utils/semantic-model-provider.ts @@ -19,10 +19,10 @@ const fixes: Record = { Array: "any[]", bloolean: undefined, Control: "sap.ui.core.Control", + "sap.m.IToolbarInteractiveControl": undefined, Element: "sap.ui.core.Element", "sap.fe.macros.MacroMetadata": undefined, "sap.gantt.misc.AxisTime": undefined, - "sap.m.IToolbarInteractiveControl": undefined, "sap.gantt.control.Toolbar": undefined, "sap.gantt.DragOrientation": undefined, "sap.gantt.simple.GanttHeader": undefined,