diff --git a/src/extension/services/validationService/checks/adb.ts b/src/extension/services/validationService/checks/adb.ts index 2418359c0..71081aebc 100644 --- a/src/extension/services/validationService/checks/adb.ts +++ b/src/extension/services/validationService/checks/adb.ts @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as semver from "semver"; -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; -import { createNotFoundMessage, createVersionErrorMessage, getVersion } from "../util"; +import { + basicCheck, + createNotFoundMessage, + createVersionErrorMessage, + parseVersion, +} from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; nls.config({ @@ -17,40 +20,43 @@ const toLocale = nls.loadMessageBundle(); const label = "ADB"; async function test(): Promise { - if (!cexists.sync("adb")) { + const result = await basicCheck({ + command: "adb", + versionRange: "30.0.0", + getVersion: parseVersion.bind(null, "adb --version", /^.*\n.*version (.*?)( |$|\n)/gi), + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - const version = await getVersion("adb --version", /^.*\n.*version (.*?)( |$|\n)/gi); - - if (!version) { + if (result.versionCompare === undefined) { return { status: "failure", comment: createVersionErrorMessage(label), }; } - const isOlder = semver.lt(version, "30.0.0"); - return isOlder - ? { - status: "partial-success", - comment: - "Detected version is older than 30.0.0. " + - "Please update SDK tools in case of errors", - } - : { - status: "success", - }; + if (result.versionCompare === -1) { + return { + status: "partial-success", + comment: + "Detected version is older than 30.0.0. " + + "Please update SDK tools in case of errors", + }; + } + + return { status: "success" }; } const adbAndroid: IValidation = { label, description: toLocale( "AdbCheckAndroidDescription", - "Required for app installition. Minimal version is 12", + "Required for app installation. Minimal version is 12", ), category: ValidationCategoryE.Android, exec: test, diff --git a/src/extension/services/validationService/checks/cocoaPods.ts b/src/extension/services/validationService/checks/cocoaPods.ts index 75ab1ab5d..2d2c0325d 100644 --- a/src/extension/services/validationService/checks/cocoaPods.ts +++ b/src/extension/services/validationService/checks/cocoaPods.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; -import { createNotFoundMessage } from "../util"; +import { basicCheck, createNotFoundMessage } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; nls.config({ @@ -16,7 +15,11 @@ const toLocale = nls.loadMessageBundle(); const label = "CocoaPods"; async function test(): Promise { - if (!cexists.sync("pod")) { + const result = await basicCheck({ + command: "pod", + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), diff --git a/src/extension/services/validationService/checks/emulator.ts b/src/extension/services/validationService/checks/emulator.ts index 228a92010..0757acf1b 100644 --- a/src/extension/services/validationService/checks/emulator.ts +++ b/src/extension/services/validationService/checks/emulator.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as semver from "semver"; -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; import { + basicCheck, createNotFoundMessage, createVersionErrorMessage, - executeCommand, - normizeStr, + parseVersion, } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; @@ -22,41 +20,36 @@ const toLocale = nls.loadMessageBundle(); const label = "Android Emulator"; async function test(): Promise { - if (!cexists.sync("emulator")) { + const result = await basicCheck({ + command: "emulator", + versionRange: "30.0.0", + getVersion: parseVersion.bind(null, "emulator -version", /version (.*?)( |$)/gi), + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - const command = "emulator -version"; - const data = await executeCommand(command); - - const text = normizeStr(data.stdout).split("\n")[0]; - const reg = /version (.*?)( |$)/gi; - - // something like '30.9.5.0' converts to '30.9.5', safe with nulls - const version = semver.coerce(reg.exec(text)?.[1]); - - if (!version) { + if (result.versionCompare === undefined) { return { status: "failure", comment: createVersionErrorMessage(label), }; } - const isOlder = semver.lt(version, "30.0.0"); + if (result.versionCompare === -1) { + return { + status: "partial-success", + comment: + "Detected version is older than 30.0.0. " + + "Please update SDK tools in case of errors", + }; + } - return isOlder - ? { - status: "partial-success", - comment: - "Detected version is older than 30.0.0. " + - "Please update SDK tools in case of errors", - } - : { - status: "success", - }; + return { status: "success" }; } const main: IValidation = { diff --git a/src/extension/services/validationService/checks/env.ts b/src/extension/services/validationService/checks/env.ts index bde8226f9..6183c71f9 100644 --- a/src/extension/services/validationService/checks/env.ts +++ b/src/extension/services/validationService/checks/env.ts @@ -17,11 +17,11 @@ const toLocale = nls.loadMessageBundle(); const convertPathWithVars = (str: string) => str.replace(/%([^%]+)%/g, (_, n) => process.env[n] || _); -const envVars = { - ANDROID_HOME: process.env.ANDROID_HOME, -}; - async function test(): Promise { + const envVars = { + ANDROID_HOME: process.env.ANDROID_HOME, + }; + const resolvedEnv = fromEntries( Object.entries(envVars).map(([key, val]) => [ key, diff --git a/src/extension/services/validationService/checks/expoCli.ts b/src/extension/services/validationService/checks/expoCli.ts index 155d251c9..53e2336cb 100644 --- a/src/extension/services/validationService/checks/expoCli.ts +++ b/src/extension/services/validationService/checks/expoCli.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; -import { createNotFoundMessage } from "../util"; +import { basicCheck, createNotFoundMessage } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; nls.config({ @@ -16,16 +15,18 @@ const toLocale = nls.loadMessageBundle(); const label = "Expo CLI"; async function test(): Promise { - if (!cexists.sync("expo-cli")) { + const result = await basicCheck({ + command: "expo-cli", + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - return { - status: "success", - }; + return { status: "success" }; } const main: IValidation = { diff --git a/src/extension/services/validationService/checks/gradle.ts b/src/extension/services/validationService/checks/gradle.ts index be404390b..c44d2fe57 100644 --- a/src/extension/services/validationService/checks/gradle.ts +++ b/src/extension/services/validationService/checks/gradle.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as semver from "semver"; -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; import { + basicCheck, createNotFoundMessage, createVersionErrorMessage, - executeCommand, - normizeStr, + parseVersion, } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; @@ -22,28 +20,25 @@ const toLocale = nls.loadMessageBundle(); const label = "Gradle"; async function test(): Promise { - if (!cexists.sync("gradle")) { + const result = await basicCheck({ + command: "gradle", + getVersion: parseVersion.bind(null, "gradle -version", /gradle (.*?)( |$)/gim), + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - const command = "gradle -version"; - const data = await executeCommand(command); - - const text = normizeStr(data.stdout).split("\n")[2]; - const reg = /gradle (.*?)( |$)/gi; - const version = semver.coerce(reg.exec(text)?.[1]); - - if (!version) { + if (result.versionCompare === undefined) { return { status: "failure", comment: createVersionErrorMessage(label), }; } - // #todo> Not sure which gradle versions are required return { status: "success", }; diff --git a/src/extension/services/validationService/checks/iosDeploy.ts b/src/extension/services/validationService/checks/iosDeploy.ts index 6c610f81c..f49f0cd79 100644 --- a/src/extension/services/validationService/checks/iosDeploy.ts +++ b/src/extension/services/validationService/checks/iosDeploy.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; -import { createNotFoundMessage } from "../util"; +import { basicCheck, createNotFoundMessage } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; nls.config({ @@ -16,7 +15,11 @@ const toLocale = nls.loadMessageBundle(); const label = "ios-deploy"; async function test(): Promise { - if (!cexists.sync("ios-deploy")) { + const result = await basicCheck({ + command: "ios-deploy", + }); + + if (!result.exists) { return { status: "partial-success", // not necessary required comment: createNotFoundMessage(label), diff --git a/src/extension/services/validationService/checks/java.ts b/src/extension/services/validationService/checks/java.ts index 4cbcfccc5..06d032e8d 100644 --- a/src/extension/services/validationService/checks/java.ts +++ b/src/extension/services/validationService/checks/java.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as semver from "semver"; -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; import { + basicCheck, createNotFoundMessage, createVersionErrorMessage, - executeCommand, - normizeStr, + parseVersion, } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; @@ -22,42 +20,28 @@ const toLocale = nls.loadMessageBundle(); const label = "Java"; async function test(): Promise { - if (!cexists.sync("java")) { + // for future changes: keep in mind that java version format has changed since Java8 + const result = await basicCheck({ + command: "java", + getVersion: parseVersion.bind(null, "java -version", /version "(.*?)"( |$|\n)/gi, "stderr"), + versionRange: "1.8.0", + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - const command = "java -version"; - - const data = await executeCommand(command); - - // https://stackoverflow.com/questions/13483443/why-does-java-version-go-to-stderr - // `java -version` goes to stderr... - const text = normizeStr(data.stderr).split("\n")[0]; - // something like 1.8.0 - // example `java -version` output: java version "16.0.1" 2021-04-20 - const vOldReg = /version "(.*?)"( |$)/gi; - // something like 11.0.12 - // this regex parses the output of `java --version`, which should not be required, - // but let's leave it here just to be sure nothing breaks in future java versions - // example `java --version` output: java 16.0.1 2021-04-20 - const vNewReg = /java (.*?)( |$)/gi; - const version = semver.coerce(vOldReg.exec(text)?.[1] || vNewReg.exec(text)?.[1]); - - if (!version) { + if (result.versionCompare === undefined) { return { status: "failure", - comment: createVersionErrorMessage("label"), + comment: createVersionErrorMessage(label), }; } - // the fact that version format has changed after java8 does not - // change this line, but it is something to keep in mind - const isOlder = semver.lt(version, "1.8.0"); - - if (isOlder) { + if (result.versionCompare === -1) { return { status: "partial-success", comment: `Detected version is older than 1.8.0. Please install ${label} 8 in case of errors`, diff --git a/src/extension/services/validationService/checks/nodeJS.ts b/src/extension/services/validationService/checks/nodeJS.ts index 4a570ca4c..20082ab85 100644 --- a/src/extension/services/validationService/checks/nodeJS.ts +++ b/src/extension/services/validationService/checks/nodeJS.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as semver from "semver"; -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; import { + basicCheck, createNotFoundMessage, createVersionErrorMessage, - executeCommand, - normizeStr, + parseVersion, } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; @@ -22,37 +20,36 @@ const toLocale = nls.loadMessageBundle(); const label = "Node.JS"; async function test(): Promise { - if (!cexists.sync("node")) { + const result = await basicCheck({ + command: "node", + versionRange: "12.0.0", + getVersion: parseVersion.bind(null, "node --version"), + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - const command = "node --version"; - const data = await executeCommand(command); - - const text = normizeStr(data.stdout).split("\n")[0]; - const version = semver.coerce(text); - if (!version) { + if (result.versionCompare === undefined) { return { status: "failure", comment: createVersionErrorMessage(label), }; } - const isOlder = semver.lt(version, "12.0.0"); + if (result.versionCompare === -1) { + return { + status: "failure", + comment: + "Detected version is older than 12.0.0 " + + `Minimal required version is 12.0.0. Please update your ${label} installation`, + }; + } - return isOlder - ? { - status: "failure", - comment: - "Detected version is older than 12.0.0 " + - `Minimal required version is 12.0.0. Please update your ${label} installation`, - } - : { - status: "success", - }; + return { status: "success" }; } const main: IValidation = { diff --git a/src/extension/services/validationService/checks/npm.ts b/src/extension/services/validationService/checks/npm.ts index 8d66ddc40..1fb51fa9b 100644 --- a/src/extension/services/validationService/checks/npm.ts +++ b/src/extension/services/validationService/checks/npm.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as semver from "semver"; -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; import { + basicCheck, createNotFoundMessage, createVersionErrorMessage, - executeCommand, - normizeStr, + parseVersion, } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; @@ -22,20 +20,19 @@ const toLocale = nls.loadMessageBundle(); const label = "NPM"; async function test(): Promise { - if (!cexists.sync("npm")) { + const result = await basicCheck({ + command: "npm", + getVersion: parseVersion.bind(null, "npm --version"), + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - const command = "npm --version"; - const data = await executeCommand(command); - - const text = normizeStr(data.stdout).split("\n")[0]; - const version = semver.coerce(text); - - if (!version) { + if (result.versionCompare === undefined) { return { status: "failure", comment: createVersionErrorMessage(label), diff --git a/src/extension/services/validationService/checks/watchman.ts b/src/extension/services/validationService/checks/watchman.ts index f961ca966..575971019 100644 --- a/src/extension/services/validationService/checks/watchman.ts +++ b/src/extension/services/validationService/checks/watchman.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; -import { createNotFoundMessage } from "../util"; +import { basicCheck, createNotFoundMessage } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; nls.config({ @@ -16,16 +15,18 @@ const toLocale = nls.loadMessageBundle(); const label = "Watchman"; async function test(): Promise { - if (!cexists.sync("watchman")) { + const result = await basicCheck({ + command: "watchman", + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - return { - status: "success", - }; + return { status: "success" }; } const main: IValidation = { diff --git a/src/extension/services/validationService/checks/xcodebuild.ts b/src/extension/services/validationService/checks/xcodebuild.ts index 627faf576..938120bee 100644 --- a/src/extension/services/validationService/checks/xcodebuild.ts +++ b/src/extension/services/validationService/checks/xcodebuild.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -import * as cexists from "command-exists"; import * as nls from "vscode-nls"; -import { createNotFoundMessage } from "../util"; +import { basicCheck, createNotFoundMessage } from "../util"; import { ValidationCategoryE, IValidation, ValidationResultT } from "./types"; nls.config({ @@ -16,16 +15,18 @@ const toLocale = nls.loadMessageBundle(); const label = "XCodeBuild"; async function test(): Promise { - if (!cexists.sync("xcodebuild")) { + const result = await basicCheck({ + command: "xcodebuild", + }); + + if (!result.exists) { return { status: "failure", comment: createNotFoundMessage(label), }; } - return { - status: "success", - }; + return { status: "success" }; } const main: IValidation = { diff --git a/src/extension/services/validationService/util.ts b/src/extension/services/validationService/util.ts index 2ce10e600..8e60f7d72 100644 --- a/src/extension/services/validationService/util.ts +++ b/src/extension/services/validationService/util.ts @@ -4,6 +4,7 @@ import * as cp from "child_process"; import { promisify } from "util"; import * as semver from "semver"; +import * as commandExists from "command-exists"; export const executeCommand = promisify(cp.exec); export const normizeStr = (str: string): string => str.replace(/\r\n/g, "\n"); @@ -13,15 +14,58 @@ export const createNotFoundMessage = (str: string): string => export const createVersionErrorMessage = (str: string): string => `Version check failed. Make sure ${str} is working correctly`; -/** Run command and parse output with regex. If command does not exist - throws an error. */ -export const getVersion = async ( +interface IBasicCheckResult { + exists: boolean; + /** + * - 0 : within range + * - 1 : gt range + * - -1 : lt range*/ + versionCompare?: 0 | 1 | -1; +} + +export const basicCheck = async (arg: { + command: string; + getVersion?: () => Promise; + versionRange?: semver.Range | string; +}): Promise => { + const result = { + exists: true, + } as IBasicCheckResult; + + if (!commandExists.sync(arg.command)) { + result.exists = false; + return result; + } + + const version = await arg.getVersion?.(); + + if (!version) { + return result; + } + + if (!arg.versionRange) { + result.versionCompare = 0; + return result; + } + + result.versionCompare = semver.gtr(version, arg.versionRange) + ? 1 + : semver.ltr(version, arg.versionRange) + ? -1 + : 0; + + return result; +}; + +/** Run command and parse output with regex. Get first capturing group. If command does not exist - throws an error. */ +export const parseVersion = async ( command: string, - reg: RegExp, + reg?: RegExp, prop: "stdout" | "stderr" = "stdout", ): Promise => { const data = await executeCommand(command); - const version = semver.coerce(reg.exec(normizeStr(data[prop]))?.[1]); - return version; + const text = normizeStr(data[prop]); + return semver.coerce(reg ? reg.exec(text)?.[1] : text); }; // change typescript lib to es2019 ? diff --git a/test/extension/checkEnvironment.test.ts b/test/extension/checkEnvironment.test.ts new file mode 100644 index 000000000..d4cf911ea --- /dev/null +++ b/test/extension/checkEnvironment.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import * as assert from "assert"; +import { promises as fs } from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as proxyquire from "proxyquire"; + +suite("checkEnvironment", function () { + suite("basicCheck", function () { + const commandExistsStub = { sync: () => true }; + const fakeUtil = proxyquire("../../src/extension/services/validationService/util", { + "command-exists": commandExistsStub, + }) as typeof import("../../src/extension/services/validationService/util"); + + test("command should exist", async () => { + commandExistsStub.sync = () => true; + assert.deepStrictEqual(await fakeUtil.basicCheck({ command: "whatever" }), { + exists: true, + }); + }); + + test("command should not exist", async () => { + commandExistsStub.sync = () => false; + assert.deepStrictEqual(await fakeUtil.basicCheck({ command: "whatever" }), { + exists: false, + }); + }); + + test("command should check version", async () => { + commandExistsStub.sync = () => true; + + let wasExecuted = false; + + assert.deepStrictEqual( + await fakeUtil.basicCheck({ + command: "whatever", + getVersion: async () => ((wasExecuted = true), "0.0.1"), + }), + { + exists: true, + versionCompare: 0, + }, + ); + + assert(wasExecuted); + }); + + test("command should compare version lt", async () => { + commandExistsStub.sync = () => true; + + assert.deepStrictEqual( + await fakeUtil.basicCheck({ + command: "whatever", + getVersion: async () => "0.0.1", + versionRange: ">0.0.1", + }), + { + exists: true, + versionCompare: -1, + }, + ); + }); + + test("command should compare version gt", async () => { + commandExistsStub.sync = () => true; + + assert.deepStrictEqual( + await fakeUtil.basicCheck({ + command: "whatever", + getVersion: async () => "0.0.1", + versionRange: "<0.0.1", + }), + { + exists: true, + versionCompare: 1, + }, + ); + }); + + test("command should compare version eq", async () => { + commandExistsStub.sync = () => true; + + assert.deepStrictEqual( + await fakeUtil.basicCheck({ + command: "whatever", + getVersion: async () => "0.0.1", + versionRange: "=0.0.1", + }), + { + exists: true, + versionCompare: 0, + }, + ); + }); + }); + + suite("envTest", async function () { + const envTest = await import( + "../../src/extension/services/validationService/checks/env" + ).then(it => it.androidHome.exec); + + const envVars = { + ANDROID_HOME: process.env.ANDROID_HOME, + }; + + const setEnv = (arg: string) => { + Object.keys(envVars).forEach(it => { + process.env[it] = arg; + }); + }; + const restoreEnv = () => { + Object.keys(envVars).forEach(it => { + process.env[it] = envVars[it]; + }); + }; + + test("should succeed on correct env", async () => { + const tempdir = await fs.mkdtemp(path.join(os.tmpdir(), "foo")); + setEnv(tempdir); + + const result = await envTest(); + + assert(result.status === "success"); + + restoreEnv(); + }); + + test("should fail on non existant path", async () => { + setEnv("/non-existant-path-abcd"); + + const result = await envTest(); + + assert(result.status === "failure"); + + restoreEnv(); + }); + + test("should succeed on path with env values", async () => { + const varName = "some-weired-variable-abcd"; + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "foo")); + await fs.mkdir(path.join(tempDir, "bar")); + + process.env[varName] = "bar"; + + setEnv(`${tempDir}/%${varName}%`); + + const result = await envTest(); + + assert(result.status === "partial-success"); + + restoreEnv(); + delete process.env[varName]; + }); + }); +});