diff --git a/src/common/PqSdkNugetPackageService.ts b/src/common/PqSdkNugetPackageService.ts index d96cfeb5..c7cfb1ce 100644 --- a/src/common/PqSdkNugetPackageService.ts +++ b/src/common/PqSdkNugetPackageService.ts @@ -22,6 +22,9 @@ import { PqSdkOutputChannel } from "../features/PqSdkOutputChannel"; export class PqSdkNugetPackageService { private readonly nugetHttpService: NugetHttpService; private readonly nugetCommandService: NugetCommandService; + private readonly nullableMaximumPqTestNugetVersion?: NugetVersions = ExtensionConstants.MaximumPqTestNugetVersion + ? NugetVersions.createFromFuzzyVersionString(ExtensionConstants.MaximumPqTestNugetVersion) + : undefined; constructor( readonly vscExtCtx: vscode.ExtensionContext, @@ -43,24 +46,36 @@ export class PqSdkNugetPackageService { ); } - public async findNullableNewPqSdkVersion(): Promise { + /** + * Return a list of nuget versions less or equal to `options.maximumNugetVersion` if it got specified + * Otherwise, return the whole list. + * @param options + */ + public async findNullableNewPqSdkVersion( + options: { + maximumNugetVersion?: NugetVersions; + } = {}, + ): Promise { let sortedNugetVersions: NugetVersions[]; + // always restrain the version found beneath the MaximumPqTestNugetVersion + if (!options.maximumNugetVersion) { + options.maximumNugetVersion = this.nullableMaximumPqTestNugetVersion; + } + if (ExtensionConfigurations.nugetPath) { - sortedNugetVersions = ( - await this.nugetCommandService.getPackageReleasedVersions( - ExtensionConfigurations.nugetPath, - ExtensionConfigurations.nugetFeed, - ExtensionConstants.PublicMsftPqSdkToolsNugetName, - ) - ).sort(NugetVersions.compare); + sortedNugetVersions = await this.nugetCommandService.getSortedPackageReleasedVersions( + ExtensionConfigurations.nugetPath, + ExtensionConfigurations.nugetFeed, + ExtensionConstants.PublicMsftPqSdkToolsNugetName, + options, + ); } else { // we gonna use http endpoint to query the public feed - sortedNugetVersions = ( - await this.nugetHttpService.getPackageReleasedVersions(ExtensionConstants.PublicMsftPqSdkToolsNugetName) - ).versions - .map((releasedVersion: string) => NugetVersions.createFromReleasedVersionString(releasedVersion)) - .sort(NugetVersions.compare); + sortedNugetVersions = await this.nugetHttpService.getSortedPackageReleasedVersions( + ExtensionConstants.PublicMsftPqSdkToolsNugetName, + options, + ); } if (sortedNugetVersions.length && !sortedNugetVersions[sortedNugetVersions.length - 1].isZero()) { diff --git a/src/common/nuget/NugetCommandService.ts b/src/common/nuget/NugetCommandService.ts index cd866c06..24f2bc41 100644 --- a/src/common/nuget/NugetCommandService.ts +++ b/src/common/nuget/NugetCommandService.ts @@ -134,6 +134,29 @@ export class NugetCommandService { ); } + public async getSortedPackageReleasedVersions( + nugetPath: string = "nuget", + nugetFeed: string | undefined = undefined, + packageName: string, + options: { + maximumNugetVersion?: NugetVersions; + } = {}, + ): Promise { + let sortedNugetVersions: NugetVersions[] = ( + await this.getPackageReleasedVersions(nugetPath, nugetFeed, packageName) + ).sort(NugetVersions.compare); + + if (options.maximumNugetVersion) { + // filter out any version gt maximumNugetVersion in sortedNugetVersions + sortedNugetVersions = sortedNugetVersions.filter( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (one: NugetVersions) => one.compare(options.maximumNugetVersion!) <= 0, + ); + } + + return sortedNugetVersions; + } + public downloadAndExtractNugetPackage( nugetPath: string = "nuget", nugetFeed: string | undefined = undefined, diff --git a/src/common/nuget/NugetHttpService.ts b/src/common/nuget/NugetHttpService.ts index 8802089e..7b8396fb 100644 --- a/src/common/nuget/NugetHttpService.ts +++ b/src/common/nuget/NugetHttpService.ts @@ -6,131 +6,24 @@ */ import * as path from "path"; -import * as stream from "stream"; import * as StreamZip from "node-stream-zip"; -import { createWriteStream, WriteStream } from "fs"; -import { promisify } from "util"; import { StreamZipAsync } from "node-stream-zip"; -import axios, { AxiosInstance, AxiosResponse } from "axios"; import { makeOneTmpDir } from "../../utils/osUtils"; -import type { PqSdkOutputChannel } from "../../features/PqSdkOutputChannel"; +import { NugetLiteHttpService } from "./NugetLiteHttpService"; +import type { PqSdkOutputChannel } from "../../features/PqSdkOutputChannel"; // this pal got vscode modules imported import { removeDirectoryRecursively } from "../../utils/files"; -const streamFinished$deferred: ( - stream: NodeJS.ReadStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, - options?: stream.FinishedOptions | undefined, -) => Promise = promisify(stream.finished); - -/** - * Format - * https: into https - * http: into http - * @param urlProtocol - */ -function formatUrlProtocol(urlProtocol: string): string { - if (urlProtocol.toLowerCase().indexOf("https") > -1) { - return "https"; - } else if (urlProtocol.toLowerCase().indexOf("http") > -1) { - return "http"; - } else { - return urlProtocol; - } -} - -export class NugetHttpService { - // public static PreReleaseIncludedVersionRegex: RegExp = /^((?:\.?[0-9]+){3,}(?:[-a-z0-9]+)?)$/; - // eslint-disable-next-line security/detect-unsafe-regex - public static ReleasedVersionRegex: RegExp = /^((?:\.?[0-9]+){3,})$/; - public static DefaultBaseUrl: string = "https://api.nuget.org"; - - private instance: AxiosInstance = axios.create({ - baseURL: NugetHttpService.DefaultBaseUrl, - }); - private errorHandler: (error: Error) => void = (error: Error) => { +export class NugetHttpService extends NugetLiteHttpService { + protected override errorHandler: (error: Error) => void = (error: Error) => { this.outputChannel?.appendErrorLine(`Failed to request to public nuget endpoints due to ${error}`); }; constructor(private readonly outputChannel?: PqSdkOutputChannel) { - this.updateAxiosInstance(); - } - - public updateAxiosInstance( - baseURL: string = NugetHttpService.DefaultBaseUrl, - nullableHttpProxy: string | undefined = undefined, - nullableHttpProxyAuthHeaderString: string | undefined = undefined, - ): void { - if (nullableHttpProxy) { - const proxyUrl: URL = new URL(nullableHttpProxy); - - this.instance = axios.create({ - baseURL, - proxy: { - protocol: formatUrlProtocol(proxyUrl.protocol), - host: proxyUrl.hostname, - port: parseInt(proxyUrl.port, 10), - }, - }); - - if (nullableHttpProxyAuthHeaderString) { - this.instance.defaults.headers.common["Authorization"] = nullableHttpProxyAuthHeaderString; - } - } else { - this.instance = axios.create({ - baseURL, - }); - } - } - - public async getPackageVersions(packageName: string): Promise<{ versions: string[] }> { - try { - const response: AxiosResponse<{ versions: string[] }> = await this.instance.get( - `v3-flatcontainer/${packageName.toLowerCase()}/index.json`, - ); - - return response.data; - } catch (e: unknown) { - this.errorHandler(e as Error); - - throw e; - } - } - - public async getPackageReleasedVersions(packageName: string): Promise<{ versions: string[] }> { - const preReleasedVersionIncludeVersions: { versions: string[] } = await this.getPackageVersions(packageName); - - preReleasedVersionIncludeVersions.versions = preReleasedVersionIncludeVersions.versions.filter( - (versionStr: string) => NugetHttpService.ReleasedVersionRegex.exec(versionStr), - ); - - return preReleasedVersionIncludeVersions; - } - - public downloadNugetPackage(packageName: string, packageVersion: string, outputLocation: string): Promise { - const writer: WriteStream = createWriteStream(outputLocation); - const packageNameInLowerCase: string = packageName.toLowerCase(); - - try { - return this.instance - .get( - `v3-flatcontainer/${packageNameInLowerCase}/${packageVersion}/${packageNameInLowerCase}.${packageVersion}.nupkg`, - { - responseType: "stream", - }, - ) - .then((response: AxiosResponse) => { - response.data.pipe(writer); - - return streamFinished$deferred(writer); - }); - } catch (e: unknown) { - this.errorHandler(e as Error); - - throw e; - } + super(); } - public async downloadAndExtractNugetPackage( + public override async downloadAndExtractNugetPackage( packageName: string, packageVersion: string, outputLocation: string, diff --git a/src/common/nuget/NugetLiteHttpService.ts b/src/common/nuget/NugetLiteHttpService.ts new file mode 100644 index 00000000..0f8f4177 --- /dev/null +++ b/src/common/nuget/NugetLiteHttpService.ts @@ -0,0 +1,178 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the MIT license found in the + * LICENSE file in the root of this projects source tree. + */ + +import * as path from "path"; +import * as stream from "stream"; +import * as StreamZip from "node-stream-zip"; + +import { createWriteStream, WriteStream } from "fs"; +import { promisify } from "util"; +import { StreamZipAsync } from "node-stream-zip"; + +import axios, { AxiosInstance, AxiosResponse } from "axios"; +import { makeOneTmpDir } from "../../utils/osUtils"; +import { NugetVersions } from "../../utils/NugetVersions"; +import { tryRemoveDirectoryRecursively } from "../../utils/files"; + +const streamFinished$deferred: ( + stream: NodeJS.ReadStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, + options?: stream.FinishedOptions | undefined, +) => Promise = promisify(stream.finished); + +/** + * Format + * https: into https + * http: into http + * @param urlProtocol + */ +function formatUrlProtocol(urlProtocol: string): string { + if (urlProtocol.toLowerCase().indexOf("https") > -1) { + return "https"; + } else if (urlProtocol.toLowerCase().indexOf("http") > -1) { + return "http"; + } else { + return urlProtocol; + } +} + +/** + * NugetLiteHttpService is reserved for testing, + * thus it cannot import any modules from 'vscode' + */ +export class NugetLiteHttpService { + // public static PreReleaseIncludedVersionRegex: RegExp = /^((?:\.?[0-9]+){3,}(?:[-a-z0-9]+)?)$/; + // eslint-disable-next-line security/detect-unsafe-regex + public static ReleasedVersionRegex: RegExp = /^((?:\.?[0-9]+){3,})$/; + public static DefaultBaseUrl: string = "https://api.nuget.org"; + + protected instance: AxiosInstance = axios.create({ + baseURL: NugetLiteHttpService.DefaultBaseUrl, + }); + protected errorHandler: (error: Error) => void = () => { + // noop + }; + constructor() { + this.updateAxiosInstance(); + } + + public updateAxiosInstance( + baseURL: string = NugetLiteHttpService.DefaultBaseUrl, + nullableHttpProxy: string | undefined = undefined, + nullableHttpProxyAuthHeaderString: string | undefined = undefined, + ): void { + if (nullableHttpProxy) { + const proxyUrl: URL = new URL(nullableHttpProxy); + + this.instance = axios.create({ + baseURL, + proxy: { + protocol: formatUrlProtocol(proxyUrl.protocol), + host: proxyUrl.hostname, + port: parseInt(proxyUrl.port, 10), + }, + }); + + if (nullableHttpProxyAuthHeaderString) { + this.instance.defaults.headers.common["Authorization"] = nullableHttpProxyAuthHeaderString; + } + } else { + this.instance = axios.create({ + baseURL, + }); + } + } + + public async getPackageVersions(packageName: string): Promise<{ versions: string[] }> { + try { + const response: AxiosResponse<{ versions: string[] }> = await this.instance.get( + `v3-flatcontainer/${packageName.toLowerCase()}/index.json`, + ); + + return response.data; + } catch (e: unknown) { + this.errorHandler(e as Error); + + throw e; + } + } + + public async getPackageReleasedVersions(packageName: string): Promise<{ versions: string[] }> { + const preReleasedVersionIncludeVersions: { versions: string[] } = await this.getPackageVersions(packageName); + + preReleasedVersionIncludeVersions.versions = preReleasedVersionIncludeVersions.versions.filter( + (versionStr: string) => NugetLiteHttpService.ReleasedVersionRegex.exec(versionStr), + ); + + return preReleasedVersionIncludeVersions; + } + + public async getSortedPackageReleasedVersions( + packageName: string, + options: { + maximumNugetVersion?: NugetVersions; + } = {}, + ): Promise { + const preReleasedVersionIncludeVersions: { versions: string[] } = await this.getPackageReleasedVersions( + packageName, + ); + + let sortedNugetVersions: NugetVersions[] = preReleasedVersionIncludeVersions.versions + .map((releasedVersion: string) => NugetVersions.createFromReleasedVersionString(releasedVersion)) + .sort(NugetVersions.compare); + + if (options.maximumNugetVersion) { + // filter out any version gt maximumNugetVersion in sortedNugetVersions + sortedNugetVersions = sortedNugetVersions.filter( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (one: NugetVersions) => one.compare(options.maximumNugetVersion!) <= 0, + ); + } + + return sortedNugetVersions; + } + + public downloadNugetPackage(packageName: string, packageVersion: string, outputLocation: string): Promise { + const writer: WriteStream = createWriteStream(outputLocation); + const packageNameInLowerCase: string = packageName.toLowerCase(); + + try { + return this.instance + .get( + `v3-flatcontainer/${packageNameInLowerCase}/${packageVersion}/${packageNameInLowerCase}.${packageVersion}.nupkg`, + { + responseType: "stream", + }, + ) + .then((response: AxiosResponse) => { + response.data.pipe(writer); + + return streamFinished$deferred(writer); + }); + } catch (e: unknown) { + this.errorHandler(e as Error); + + throw e; + } + } + + public async downloadAndExtractNugetPackage( + packageName: string, + packageVersion: string, + outputLocation: string, + ): Promise { + const oneTmpDir: string = makeOneTmpDir(); + + const targetFilePath: string = path.join(oneTmpDir, `${packageName}.${packageVersion}.zip`); + await this.downloadNugetPackage(packageName, packageVersion, targetFilePath); + + const zip: StreamZipAsync = new StreamZip.async({ file: targetFilePath }); + await zip.extract(null, outputLocation); + await zip.close(); + + await tryRemoveDirectoryRecursively(oneTmpDir); + } +} diff --git a/src/constants/PowerQuerySdkExtension.ts b/src/constants/PowerQuerySdkExtension.ts index b7c96c25..92b7ebf5 100644 --- a/src/constants/PowerQuerySdkExtension.ts +++ b/src/constants/PowerQuerySdkExtension.ts @@ -61,7 +61,15 @@ const PQDebugType: string = PowerQueryTaskType; const NugetBaseFolder: string = ".nuget" as const; const InternalMsftPqSdkToolsNugetName: string = "Microsoft.PowerQuery.SdkTools" as const; const PublicMsftPqSdkToolsNugetName: string = InternalMsftPqSdkToolsNugetName; -const SuggestedPqTestNugetVersion: string = "2.109.5" as const; +/** + * 2.112 or 2.112.x wil limit the version of the sdkTool seized beneath 2.113 + */ +const MaximumPqTestNugetVersion: string = "2.112.x" as const; +/** + * A suggestedPqTestNugetVersion that would be used as the initially tried pqTest version + * thus, make sure it is lower than `MaximumPqTestNugetVersion` if it were specified + */ +const SuggestedPqTestNugetVersion: string = "2.112.4" as const; const PqTestSubPath: string[] = [ `${InternalMsftPqSdkToolsNugetName}.${SuggestedPqTestNugetVersion}`, @@ -99,6 +107,7 @@ export const ExtensionConstants = Object.freeze({ InternalMsftPqSdkToolsNugetName, PublicMsftPqSdkToolsNugetName, SuggestedPqTestNugetVersion, + MaximumPqTestNugetVersion, PqTestSubPath, MakePQXExecutableName, buildNugetPackageSubPath, diff --git a/src/test/common.ts b/src/test/common.ts index d5fc4e10..8144cfee 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -45,3 +45,4 @@ export const buildPqSdkSubPath: (version: string) => string[] = (version: string ExtensionConstants.buildNugetPackageSubPath(ExtensionConstants.InternalMsftPqSdkToolsNugetName, version); export const PublicMsftPqSdkToolsNugetName: string = ExtensionConstants.PublicMsftPqSdkToolsNugetName; +export const MaximumPqTestNugetVersion: string = ExtensionConstants.MaximumPqTestNugetVersion; diff --git a/src/test/utils/NugetLiteHttpService.ts b/src/test/utils/NugetLiteHttpService.ts deleted file mode 100644 index 25af9b7f..00000000 --- a/src/test/utils/NugetLiteHttpService.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the MIT license found in the - * LICENSE file in the root of this projects source tree. - */ - -import * as path from "path"; -import * as stream from "stream"; -import * as StreamZip from "node-stream-zip"; - -import { createWriteStream, WriteStream } from "fs"; -import { promisify } from "util"; -import { StreamZipAsync } from "node-stream-zip"; - -import axios, { AxiosInstance, AxiosResponse } from "axios"; -import { makeOneTmpDir } from "../../utils/osUtils"; -import { tryRemoveDirectoryRecursively } from "../../utils/files"; - -const streamFinished$deferred: ( - stream: NodeJS.ReadStream | NodeJS.WritableStream | NodeJS.ReadWriteStream, - options?: stream.FinishedOptions | undefined, -) => Promise = promisify(stream.finished); - -export class NugetLiteHttpService { - // public static PreReleaseIncludedVersionRegex: RegExp = /^((?:\.?[0-9]+){3,}(?:[-a-z0-9]+)?)$/; - // eslint-disable-next-line security/detect-unsafe-regex - public static ReleasedVersionRegex: RegExp = /^((?:\.?[0-9]+){3,})$/; - - private instance: AxiosInstance; - private errorHandler: (error: Error) => void = () => { - // noop - }; - constructor() { - this.instance = axios.create({ - baseURL: "https://api.nuget.org", - }); - } - - public async getPackageVersions(packageName: string): Promise<{ versions: string[] }> { - try { - const response: AxiosResponse<{ versions: string[] }> = await this.instance.get( - `v3-flatcontainer/${packageName.toLowerCase()}/index.json`, - ); - - return response.data; - } catch (e: unknown) { - this.errorHandler(e as Error); - - return { versions: [] }; - } - } - - public async getPackageReleasedVersions(packageName: string): Promise<{ versions: string[] }> { - const preReleasedVersionIncludeVersions: { versions: string[] } = await this.getPackageVersions(packageName); - - preReleasedVersionIncludeVersions.versions = preReleasedVersionIncludeVersions.versions.filter( - (versionStr: string) => NugetLiteHttpService.ReleasedVersionRegex.exec(versionStr), - ); - - return preReleasedVersionIncludeVersions; - } - - public downloadNugetPackage(packageName: string, packageVersion: string, outputLocation: string): Promise { - const writer: WriteStream = createWriteStream(outputLocation); - const packageNameInLowerCase: string = packageName.toLowerCase(); - - return this.instance - .get( - `v3-flatcontainer/${packageNameInLowerCase}/${packageVersion}/${packageNameInLowerCase}.${packageVersion}.nupkg`, - { - responseType: "stream", - }, - ) - .then((response: AxiosResponse) => { - response.data.pipe(writer); - - return streamFinished$deferred(writer); - }); - } - - public async downloadAndExtractNugetPackage( - packageName: string, - packageVersion: string, - outputLocation: string, - ): Promise { - const oneTmpDir: string = makeOneTmpDir(); - - const targetFilePath: string = path.join(oneTmpDir, `${packageName}.${packageVersion}.zip`); - await this.downloadNugetPackage(packageName, packageVersion, targetFilePath); - - const zip: StreamZipAsync = new StreamZip.async({ file: targetFilePath }); - await zip.extract(null, outputLocation); - await zip.close(); - - await tryRemoveDirectoryRecursively(oneTmpDir); - } -} diff --git a/src/test/utils/pqSdkNugetPackageUtils.ts b/src/test/utils/pqSdkNugetPackageUtils.ts index ebf91040..010a1d0b 100644 --- a/src/test/utils/pqSdkNugetPackageUtils.ts +++ b/src/test/utils/pqSdkNugetPackageUtils.ts @@ -13,19 +13,24 @@ import { AWAIT_INTERVAL, buildPqSdkSubPath, MAX_AWAIT_TIME, + MaximumPqTestNugetVersion, NugetPackagesDirectory, PqTestSubPath, PublicMsftPqSdkToolsNugetName, } from "../common"; import { delay } from "../../utils/pids"; -import { NugetLiteHttpService } from "./NugetLiteHttpService"; +import { NugetLiteHttpService } from "../../common/nuget/NugetLiteHttpService"; import { NugetVersions } from "../../utils/NugetVersions"; const nugetHttpService = new NugetLiteHttpService(); const expect = chai.expect; +const MaximumPqTestNugetVersions: NugetVersions | undefined = MaximumPqTestNugetVersion + ? NugetVersions.createFromFuzzyVersionString(MaximumPqTestNugetVersion) + : undefined; + export module PqSdkNugetPackages { let latestPQSdkNugetVersion: NugetVersions | undefined = undefined; @@ -36,13 +41,16 @@ export module PqSdkNugetPackages { } export async function getAllPQSdkVersions(): Promise { - const releasedVersions = await nugetHttpService.getPackageReleasedVersions(PublicMsftPqSdkToolsNugetName); + const releasedVersions = await nugetHttpService.getSortedPackageReleasedVersions( + PublicMsftPqSdkToolsNugetName, + { + maximumNugetVersion: MaximumPqTestNugetVersions, + }, + ); - expect(releasedVersions.versions.length).gt(0); + expect(releasedVersions.length).gt(0); - return releasedVersions.versions.map((releasedVersion: string) => - NugetVersions.createFromReleasedVersionString(releasedVersion), - ); + return releasedVersions; } export async function assertPqSdkToolExisting(): Promise { diff --git a/src/utils/NugetVersions.ts b/src/utils/NugetVersions.ts index 6807aa37..50692182 100644 --- a/src/utils/NugetVersions.ts +++ b/src/utils/NugetVersions.ts @@ -9,9 +9,10 @@ import * as path from "path"; const NugetStdOutputOfVersionRegExp: RegExp = /(Microsoft\.PowerQuery\.SdkTools[ ])([0-9]+)\.([0-9]+)\.([0-9]+)/g; const PathPartOfVersionRegExp: RegExp = /(Microsoft\.PowerQuery\.SdkTools\.)([0-9]+)\.([0-9]+)\.([0-9]+)/g; -const ReleasedVersionRegExp: RegExp = /([0-9]+)\.([0-9]+)\.([0-9]+)/; +const ReleasedVersionRegExp: RegExp = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; +const NumericRegExp: RegExp = /^[0-9]+$/; export class NugetVersions { - public static ZERO_VERSION: NugetVersions = new NugetVersions(0, 0, 0); + public static ZERO_VERSION: NugetVersions = new NugetVersions("0", "0", "0"); /** * create NugetVersion from the std output like: @@ -28,7 +29,7 @@ export class NugetVersions { const matched: RegExpMatchArray | null = NugetStdOutputOfVersionRegExp.exec(stdOutput); if (matched && matched.length === 5) { - result = new NugetVersions(parseInt(matched[2], 10), parseInt(matched[3], 10), parseInt(matched[4], 10)); + result = new NugetVersions(matched[2], matched[3], matched[4]); } return result; @@ -52,9 +53,7 @@ export class NugetVersions { let matched: RegExpMatchArray | null = NugetStdOutputOfVersionRegExp.exec(stdOutput); while (matched && matched.length === 5) { - result.push( - new NugetVersions(parseInt(matched[2], 10), parseInt(matched[3], 10), parseInt(matched[4], 10)), - ); + result.push(new NugetVersions(matched[2], matched[3], matched[4])); matched = NugetStdOutputOfVersionRegExp.exec(stdOutput); } @@ -77,11 +76,7 @@ export class NugetVersions { const matched: RegExpMatchArray | null = PathPartOfVersionRegExp.exec(onePath); if (matched && matched.length === 5) { - result = new NugetVersions( - parseInt(matched[2], 10), - parseInt(matched[3], 10), - parseInt(matched[4], 10), - ); + result = new NugetVersions(matched[2], matched[3], matched[4]); return true; } @@ -103,25 +98,70 @@ export class NugetVersions { const matched: RegExpMatchArray | null = ReleasedVersionRegExp.exec(releasedVersionString); if (matched && matched.length === 4) { - result = new NugetVersions(parseInt(matched[1], 10), parseInt(matched[2], 10), parseInt(matched[3], 10)); + result = new NugetVersions(matched[1], matched[2], matched[3]); + } + + return result; + } + + /** + * create NugetVersion from a version like: + * 2.107.1 + * 2 + * 2.x + * 2.107 + * 2.107.x + * @param fuzzyVersionString + */ + public static createFromFuzzyVersionString(fuzzyVersionString: string): NugetVersions { + if (!fuzzyVersionString) return NugetVersions.ZERO_VERSION; + + let result: NugetVersions = NugetVersions.ZERO_VERSION; + const spitedString: string[] = fuzzyVersionString.split("."); + + if (spitedString && spitedString.length > 0) { + result = new NugetVersions(spitedString[0], spitedString[1] ?? "", spitedString[2] ?? ""); } return result; } public static compare(l: NugetVersions, r: NugetVersions): number { - if (l.major == r.major) { - if (l.minor == r.minor) { - return l.patch - r.patch; - } else { - return l.minor - r.minor; - } + return ( + NugetVersions.compareIdentifiers(l.major, r.major) || + NugetVersions.compareIdentifiers(l.minor, r.minor) || + NugetVersions.compareIdentifiers(l.patch, r.patch) + ); + } + + private static compareIdentifiers(l: string, r: string): number { + const isLNumber: boolean = NumericRegExp.test(l); + const isRNumber: boolean = NumericRegExp.test(r); + + let lNumber: number = -1; + let rNumber: number = -1; + + if (isLNumber && isRNumber) { + lNumber = parseInt(l, 10); + rNumber = parseInt(r, 10); + } + + if (l === r) { + return 0; + } else if (isLNumber && !isRNumber) { + return -1; + } else if (!isLNumber && isRNumber) { + return 1; } else { - return l.major - r.major; + return lNumber - rNumber; } } - constructor(public readonly major: number, public readonly minor: number, public readonly patch: number) {} + constructor(public readonly major: string, public readonly minor: string, public readonly patch: string) {} + + compare(other: NugetVersions): number { + return NugetVersions.compare(this, other); + } isZero(): boolean { return this === NugetVersions.ZERO_VERSION; diff --git a/unit-tests/common/nuget/NugetHttpService.spec.ts b/unit-tests/common/nuget/NugetHttpService.spec.ts index 5e63db02..bfcbe4ef 100644 --- a/unit-tests/common/nuget/NugetHttpService.spec.ts +++ b/unit-tests/common/nuget/NugetHttpService.spec.ts @@ -11,6 +11,7 @@ import * as path from "path"; import { makeOneTmpDir } from "../../../src/utils/osUtils"; import { NugetHttpService } from "../../../src/common/nuget/NugetHttpService"; +import { NugetVersions } from "../../../src/utils/NugetVersions"; import { tryRemoveDirectoryRecursively } from "../../../src/utils/files"; const expect = chai.expect; @@ -24,6 +25,23 @@ describe("NugetHttpService unit testes", () => { expect(res.versions.length).gt(1); }).timeout(3e4); + it("getSortedPackageReleasedVersions v1", async () => { + const allVersions: NugetVersions[] = await nugetHttpService.getSortedPackageReleasedVersions(SdkPackageName); + expect(allVersions.length).gt(1); + + const _2_110_Versions: NugetVersions[] = await nugetHttpService.getSortedPackageReleasedVersions( + SdkPackageName, + { + maximumNugetVersion: NugetVersions.createFromFuzzyVersionString("2.110.x"), + }, + ); + + expect(_2_110_Versions.length).gt(1); + expect(_2_110_Versions.length).lt(allVersions.length); + expect(_2_110_Versions[_2_110_Versions.length - 1].minor).eq("110"); + expect(parseInt(allVersions[_2_110_Versions.length].minor, 10)).gt(110); + }).timeout(3e4); + it("downloadAndExtractNugetPackage v1", async () => { const oneTmpDir = makeOneTmpDir(); const res = await nugetHttpService.getPackageReleasedVersions(SdkPackageName); diff --git a/unit-tests/common/nuget/NugetVersions.spec.ts b/unit-tests/common/nuget/NugetVersions.spec.ts new file mode 100644 index 00000000..c81fbc1d --- /dev/null +++ b/unit-tests/common/nuget/NugetVersions.spec.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the MIT license found in the + * LICENSE file in the root of this projects source tree. + */ + +import * as chai from "chai"; +import { NugetVersions } from "../../../src/utils/NugetVersions"; + +const expect = chai.expect; + +describe("NugetVersions.spec unit testes", () => { + it("createFromReleasedVersionString v1", () => { + const nugetVersions = NugetVersions.createFromReleasedVersionString("2.107.1"); + expect(nugetVersions.major).eq("2"); + expect(nugetVersions.minor).eq("107"); + expect(nugetVersions.patch).eq("1"); + }); + + it("createFromFuzzyVersionString v1", () => { + const nugetVersions = NugetVersions.createFromFuzzyVersionString("2.107.1"); + expect(nugetVersions.major).eq("2"); + expect(nugetVersions.minor).eq("107"); + expect(nugetVersions.patch).eq("1"); + }); + + it("createFromFuzzyVersionString v2", () => { + const nugetVersionsJunior = NugetVersions.createFromFuzzyVersionString("2.107.1"); + const nugetVersionsSenior = NugetVersions.createFromFuzzyVersionString("2.108.1"); + const nugetVersions = NugetVersions.createFromFuzzyVersionString("2.107.x"); + expect(nugetVersions.major).eq("2"); + expect(nugetVersions.minor).eq("107"); + expect(nugetVersions.patch).eq("x"); + expect(nugetVersionsJunior.compare(nugetVersions)).lt(0); + expect(nugetVersionsSenior.compare(nugetVersions)).gt(0); + }); + + it("createFromFuzzyVersionString v3", () => { + const nugetVersionsJunior = NugetVersions.createFromFuzzyVersionString("2.107.1"); + const nugetVersionsSenior = NugetVersions.createFromFuzzyVersionString("2.108.1"); + const nugetVersions = NugetVersions.createFromFuzzyVersionString("2.107"); + expect(nugetVersions.major).eq("2"); + expect(nugetVersions.minor).eq("107"); + expect(nugetVersions.patch).eq(""); + expect(nugetVersionsJunior.compare(nugetVersions)).lt(0); + expect(nugetVersionsSenior.compare(nugetVersions)).gt(0); + }); + + it("createFromFuzzyVersionString v4", () => { + const nugetVersionsJunior = NugetVersions.createFromFuzzyVersionString("2.108.1"); + const nugetVersionsSenior = NugetVersions.createFromFuzzyVersionString("3.108.1"); + const nugetVersions = NugetVersions.createFromFuzzyVersionString("2.x"); + expect(nugetVersions.major).eq("2"); + expect(nugetVersions.minor).eq("x"); + expect(nugetVersions.patch).eq(""); + expect(nugetVersionsJunior.compare(nugetVersions)).lt(0); + expect(nugetVersionsSenior.compare(nugetVersions)).gt(0); + }); + + it("createFromFuzzyVersionString v5", () => { + const nugetVersionsJunior = NugetVersions.createFromFuzzyVersionString("2.108.1"); + const nugetVersionsSenior = NugetVersions.createFromFuzzyVersionString("3.108.1"); + const nugetVersions = NugetVersions.createFromFuzzyVersionString("2"); + expect(nugetVersions.major).eq("2"); + expect(nugetVersions.minor).eq(""); + expect(nugetVersions.patch).eq(""); + expect(nugetVersionsJunior.compare(nugetVersions)).lt(0); + expect(nugetVersionsSenior.compare(nugetVersions)).gt(0); + }); +});