From 774de786910abd0c83e5b5e285961fcb07c38474 Mon Sep 17 00:00:00 2001 From: Norman Breau Date: Sat, 27 Mar 2021 10:05:50 -0300 Subject: [PATCH] refactor: java checks (#1130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: エリス Co-authored-by: Raphael von der Grün Update spec/unit/java.spec.js Co-authored-by: Raphael von der Grün Update spec/unit/java.spec.js Co-authored-by: Raphael von der Grün Update bin/templates/cordova/lib/utils.js Co-authored-by: Raphael von der Grün Update bin/templates/cordova/lib/check_reqs.js Co-authored-by: Raphael von der Grün Update spec/unit/check_reqs.spec.js Co-authored-by: Raphael von der Grün Update spec/unit/check_reqs.spec.js Co-authored-by: Raphael von der Grün Co-authored-by: Raphael von der Grün --- bin/templates/cordova/lib/check_reqs.js | 107 +++----------- bin/templates/cordova/lib/env/java.js | 123 ++++++++++++++++ bin/templates/cordova/lib/utils.js | 13 ++ package-lock.json | 43 +++++- package.json | 1 + spec/unit/check_reqs.spec.js | 30 +--- spec/unit/java.spec.js | 177 ++++++++++++++++++++++++ 7 files changed, 377 insertions(+), 117 deletions(-) create mode 100644 bin/templates/cordova/lib/env/java.js create mode 100644 spec/unit/java.spec.js diff --git a/bin/templates/cordova/lib/check_reqs.js b/bin/templates/cordova/lib/check_reqs.js index 10d660e9..25c3f736 100644 --- a/bin/templates/cordova/lib/check_reqs.js +++ b/bin/templates/cordova/lib/check_reqs.js @@ -20,30 +20,20 @@ const execa = require('execa'); var path = require('path'); var fs = require('fs-extra'); -var os = require('os'); -var which = require('which'); -const glob = require('fast-glob'); +const { forgivingWhichSync, isWindows, isDarwin } = require('./utils'); +const java = require('./env/java'); var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..'); var PROJECT_ROOT = path.join(__dirname, '..', '..'); const { CordovaError, ConfigParser, events } = require('cordova-common'); var android_sdk = require('./android_sdk'); const { createEditor } = require('properties-parser'); +const semver = require('semver'); -function forgivingWhichSync (cmd) { - const whichResult = which.sync(cmd, { nothrow: true }); +const EXPECTED_JAVA_VERSION = '1.8.x'; - // On null, returns empty string to maintain backwards compatibility - // realpathSync follows symlinks - return whichResult === null ? '' : fs.realpathSync(whichResult); -} - -module.exports.isWindows = function () { - return (os.platform() === 'win32'); -}; - -module.exports.isDarwin = function () { - return (os.platform() === 'darwin'); -}; +// Re-exporting these for backwards compatibility and for unit testing. +// TODO: Remove uses and use the ./utils module directly. +Object.assign(module.exports, { isWindows, isDarwin }); /** * @description Get valid target from framework/project.properties if run from this repo @@ -143,72 +133,18 @@ module.exports.check_gradle = function () { 'in your path, or install Android Studio')); }; -// Returns a promise. -module.exports.check_java = function () { - var javacPath = forgivingWhichSync('javac'); - var hasJavaHome = !!process.env.JAVA_HOME; - return Promise.resolve().then(function () { - if (hasJavaHome) { - // Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh). - if (!javacPath) { - process.env.PATH += path.delimiter + path.join(process.env.JAVA_HOME, 'bin'); - } - } else { - if (javacPath) { - // OS X has a command for finding JAVA_HOME. - var find_java = '/usr/libexec/java_home'; - var default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.'; - if (fs.existsSync(find_java)) { - return execa(find_java).then(({ stdout }) => { - process.env.JAVA_HOME = stdout; - }).catch(function (err) { - if (err) { - throw new CordovaError(default_java_error_msg); - } - }); - } else { - // See if we can derive it from javac's location. - var maybeJavaHome = path.dirname(path.dirname(javacPath)); - if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { - process.env.JAVA_HOME = maybeJavaHome; - } else { - throw new CordovaError(default_java_error_msg); - } - } - } else if (module.exports.isWindows()) { - const { env } = process; - const baseDirs = [env.ProgramFiles, env['ProgramFiles(x86)']]; - const globOpts = { absolute: true, onlyDirectories: true }; - const flatMap = (arr, f) => [].concat(...arr.map(f)); +/** + * Checks for the java installation and correct version + */ +module.exports.check_java = async function () { + const javaVersion = await java.getVersion(); - const jdkDir = flatMap(baseDirs, cwd => - glob.sync('java/jdk*', { cwd, ...globOpts }) - )[0]; - - if (jdkDir) { - env.PATH += path.delimiter + path.join(jdkDir, 'bin'); - env.JAVA_HOME = path.normalize(jdkDir); - } - } - } - }).then(function () { - return execa('javac', ['-version'], { all: true }) - .then(({ all: output }) => { - // Java <= 8 writes version info to stderr, Java >= 9 to stdout - const match = /javac\s+([\d.]+)/i.exec(output); - return match && match[1]; - }, () => { - var msg = - 'Failed to run "javac -version", make sure that you have a JDK version 8 installed.\n' + - 'You can get it from the following location:\n' + - 'https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html'; - if (process.env.JAVA_HOME) { - msg += '\n\n'; - msg += 'Your JAVA_HOME is invalid: ' + process.env.JAVA_HOME; - } - throw new CordovaError(msg); - }); - }); + if (!semver.satisfies(javaVersion.version, EXPECTED_JAVA_VERSION)) { + throw new CordovaError( + `Requirements check failed for JDK ${EXPECTED_JAVA_VERSION}! Detected version: ${javaVersion.version}\n` + + 'Check your ANDROID_SDK_ROOT / JAVA_HOME / PATH environment variables.' + ); + } }; // Returns a promise. @@ -384,13 +320,6 @@ module.exports.run = function () { return Promise.all([this.check_java(), this.check_android()]).then(function (values) { console.log('Using Android SDK: ' + process.env.ANDROID_SDK_ROOT); - if (!String(values[0]).startsWith('1.8.')) { - throw new CordovaError( - 'Requirements check failed for JDK 8 (\'1.8.*\')! Detected version: ' + values[0] + '\n' + - 'Check your ANDROID_SDK_ROOT / JAVA_HOME / PATH environment variables.' - ); - } - if (!values[1]) { throw new CordovaError('Requirements check failed for Android SDK! Android SDK was not detected.'); } diff --git a/bin/templates/cordova/lib/env/java.js b/bin/templates/cordova/lib/env/java.js new file mode 100644 index 00000000..6eed6f60 --- /dev/null +++ b/bin/templates/cordova/lib/env/java.js @@ -0,0 +1,123 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +const execa = require('execa'); +const fs = require('fs-extra'); +const path = require('path'); +const glob = require('fast-glob'); +const { CordovaError, events } = require('cordova-common'); +const utils = require('../utils'); +const semver = require('semver'); + +/** + * Will be set to true on successful ensureness. + * If true, skips the expensive java checks. + */ +let javaIsEnsured = false; + +const java = { + /** + * Gets the version from the javac executable. + * + * @returns {semver.SemVer} + */ + getVersion: async () => { + await java._ensure(process.env); + + // Java <= 8 writes version info to stderr, Java >= 9 to stdout + let version = null; + try { + version = (await execa('javac', ['-version'], { all: true })).all; + } catch (ex) { + events.emit('verbose', ex.shortMessage); + + let msg = + 'Failed to run "javac -version", make sure that you have a JDK version 8 installed.\n' + + 'You can get it from the following location:\n' + + 'https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html'; + if (process.env.JAVA_HOME) { + msg += '\n\n'; + msg += 'Your JAVA_HOME is invalid: ' + process.env.JAVA_HOME; + } + throw new CordovaError(msg); + } + + return semver.coerce(version); + }, + + /** + * Ensures that Java is installed. Will throw exception if not. + * Will set JAVA_HOME and PATH environment variables. + * + * This function becomes a no-op if already ran previously. + */ + _ensure: async (environment) => { + if (javaIsEnsured) { + return; + } + + const javacPath = utils.forgivingWhichSync('javac'); + const hasJavaHome = !!environment.JAVA_HOME; + if (hasJavaHome) { + // Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh). + if (!javacPath) { + environment.PATH += path.delimiter + path.join(environment.JAVA_HOME, 'bin'); + } + } else { + if (javacPath) { + // OS X has a command for finding JAVA_HOME. + const find_java = '/usr/libexec/java_home'; + const default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.'; + if (fs.existsSync(find_java)) { + try { + environment.JAVA_HOME = (await execa(find_java)).stdout; + } catch (ex) { + events.emit('verbose', ex.shortMessage); + throw new CordovaError(default_java_error_msg); + } + } else { + // See if we can derive it from javac's location. + var maybeJavaHome = path.dirname(path.dirname(javacPath)); + if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { + environment.JAVA_HOME = maybeJavaHome; + } else { + throw new CordovaError(default_java_error_msg); + } + } + } else if (utils.isWindows()) { + const baseDirs = [environment.ProgramFiles, environment['ProgramFiles(x86)']]; + const globOpts = { absolute: true, onlyDirectories: true }; + const flatMap = (arr, f) => [].concat(...arr.map(f)); + const jdkDir = flatMap(baseDirs, cwd => { + return glob.sync('java/jdk*', { cwd, ...globOpts }); + } + )[0]; + + if (jdkDir) { + environment.PATH += path.delimiter + path.join(jdkDir, 'bin'); + environment.JAVA_HOME = path.normalize(jdkDir); + } + } + } + + javaIsEnsured = true; + } +}; + +module.exports = java; diff --git a/bin/templates/cordova/lib/utils.js b/bin/templates/cordova/lib/utils.js index 9692b801..7436cac5 100644 --- a/bin/templates/cordova/lib/utils.js +++ b/bin/templates/cordova/lib/utils.js @@ -24,6 +24,8 @@ // TODO: Perhaps this should live in cordova-common? const fs = require('fs-extra'); +const which = require('which'); +const os = require('os'); /** * Reads, searches, and replaces the found occurences with replacementString and then writes the file back out. @@ -53,3 +55,14 @@ exports.compareByAll = fns => { return 0; }; }; + +exports.forgivingWhichSync = (cmd) => { + const whichResult = which.sync(cmd, { nothrow: true }); + + // On null, returns empty string to maintain backwards compatibility + // realpathSync follows symlinks + return whichResult === null ? '' : fs.realpathSync(whichResult); +}; + +exports.isWindows = () => os.platform() === 'win32'; +exports.isDarwin = () => os.platform() === 'darwin'; diff --git a/package-lock.json b/package-lock.json index b79ee4cc..a368dd5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,12 @@ "chalk": "^2.0.0", "js-tokens": "^4.0.0" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -481,6 +487,13 @@ "integrity": "sha512-/GWUAqa2OJNlDF5VGSe3SR1QMHEPXxx54Ur56r0qQC0H9FlBr7kyBF2SgVEhzFCPbrW4UcYgVuWrq/2Ty3QvXg==", "requires": { "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "ansi": { @@ -2122,6 +2135,14 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -2238,6 +2259,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "npm-run-path": { @@ -2771,9 +2800,12 @@ "integrity": "sha1-dLbTPJrh4AFRDxeakRaFiPGu2qk=" }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } }, "set-blocking": { "version": "2.0.0", @@ -3271,6 +3303,11 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "yargs": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", diff --git a/package.json b/package.json index eace6e1b..4f59cc38 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "is-path-inside": "^3.0.2", "nopt": "^4.0.3", "properties-parser": "^0.3.1", + "semver": "^7.3.4", "which": "^2.0.2" }, "devDependencies": { diff --git a/spec/unit/check_reqs.spec.js b/spec/unit/check_reqs.spec.js index 491bd430..91ac0700 100644 --- a/spec/unit/check_reqs.spec.js +++ b/spec/unit/check_reqs.spec.js @@ -19,11 +19,11 @@ var rewire = require('rewire'); var android_sdk = require('../../bin/templates/cordova/lib/android_sdk'); -var os = require('os'); var fs = require('fs-extra'); var path = require('path'); var events = require('cordova-common').events; var which = require('which'); +const { CordovaError } = require('cordova-common'); // This should match /bin/templates/project/build.gradle const DEFAULT_TARGET_API = 29; @@ -48,33 +48,13 @@ describe('check_reqs', function () { }); describe('check_java', () => { - let tmpDir; - beforeEach(() => { - const tmpDirTemplate = path.join(os.tmpdir(), 'cordova-android-test-'); - tmpDir = fs.realpathSync(fs.mkdtempSync(tmpDirTemplate)); - }); - afterEach(() => { - fs.removeSync(tmpDir); - }); - - it('detects JDK in default location on windows', async () => { - check_reqs.isWindows = () => true; + it('detects if unexpected JDK version is installed', async () => { check_reqs.__set__({ - execa: async () => ({}), - forgivingWhichSync: () => '' + EXPECTED_JAVA_VERSION: '9999.9999.9999', + java: { getVersion: async () => ({ version: '1.8.0' }) } }); - delete process.env.JAVA_HOME; - process.env.ProgramFiles = tmpDir; - - const jdkDir = path.join(tmpDir, 'java/jdk1.6.0_02'); - fs.ensureDirSync(jdkDir); - - await check_reqs.check_java(); - - expect(process.env.JAVA_HOME).toBe(jdkDir); - expect(process.env.PATH.split(path.delimiter)) - .toContain(path.join(jdkDir, 'bin')); + await expectAsync(check_reqs.check_java()).toBeRejectedWithError(CordovaError, /Requirements check failed for JDK 9999.9999.9999! Detected version: 1.8.0/); }); }); diff --git a/spec/unit/java.spec.js b/spec/unit/java.spec.js new file mode 100644 index 00000000..0b011791 --- /dev/null +++ b/spec/unit/java.spec.js @@ -0,0 +1,177 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +const path = require('path'); +const rewire = require('rewire'); +const { CordovaError } = require('cordova-common'); +const utils = require('../../bin/templates/cordova/lib/utils'); +const glob = require('fast-glob'); + +describe('Java', () => { + const Java = rewire('../../bin/templates/cordova/lib/env/java'); + + describe('getVersion', () => { + let originalEnsureFunc = null; + + beforeEach(() => { + /* + This is to avoid changing/polluting the + process environment variables + as a result of running these java tests; which could produce + unexpected side effects to other tests. + */ + originalEnsureFunc = Java._ensure; + spyOn(Java, '_ensure').and.callFake(() => { + return originalEnsureFunc({}); + }); + }); + + it('runs', async () => { + Java.__set__('execa', () => Promise.resolve({ + all: 'javac 1.8.0_275' + })); + + console.log('BEFORE', process.env.JAVA_HOME); + const result = await Java.getVersion(); + console.log('AFTER', process.env.JAVA_HOME); + expect(result.major).toBe(1); + expect(result.minor).toBe(8); + expect(result.patch).toBe(0); + expect(result.version).toBe('1.8.0'); + }); + + it('produces a CordovaError on error', async () => { + Java.__set__('execa', () => Promise.reject({ + shortMessage: 'test error' + })); + const emitSpy = jasmine.createSpy('events.emit'); + Java.__set__('events', { + emit: emitSpy + }); + + await expectAsync(Java.getVersion()) + .toBeRejectedWithError(CordovaError, /Failed to run "javac -version"/); + expect(emitSpy).toHaveBeenCalledWith('verbose', 'test error'); + }); + }); + + describe('_ensure', () => { + beforeEach(() => { + Java.__set__('javaIsEnsured', false); + }); + + it('with JAVA_HOME / without javac', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue(''); + + const env = { + JAVA_HOME: '/tmp/jdk' + }; + + await Java._ensure(env); + + expect(env.PATH.split(path.delimiter)) + .toContain(path.join(env.JAVA_HOME, 'bin')); + }); + + it('detects JDK in default location on windows', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue(''); + spyOn(utils, 'isWindows').and.returnValue(true); + + const root = 'C:\\Program Files'; + const env = { + ProgramFiles: root + }; + + spyOn(glob, 'sync').and.returnValue(`${root}\\java\\jdk1.8.0_275`); + + const jdkDir = `${root}\\java\\jdk1.8.0_275`; + + await Java._ensure(env); + + expect(env.JAVA_HOME).withContext('JAVA_HOME').toBe(jdkDir); + expect(env.PATH).toContain(jdkDir); + }); + + it('detects JDK in default location on windows (x86)', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue(''); + spyOn(utils, 'isWindows').and.returnValue(true); + + const root = 'C:\\Program Files (x86)'; + const env = { + 'ProgramFiles(x86)': root + }; + + spyOn(glob, 'sync').and.returnValue(`${root}\\java\\jdk1.8.0_275`); + + const jdkDir = `${root}\\java\\jdk1.8.0_275`; + + await Java._ensure(env); + + expect(env.JAVA_HOME).withContext('JAVA_HOME').toBe(jdkDir); + expect(env.PATH).toContain(jdkDir); + }); + + it('without JAVA_HOME / with javac - Mac OS X - success', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin'); + const fsSpy = jasmine.createSpy('fs').and.returnValue(true); + Java.__set__('fs', { + existsSync: fsSpy + }); + Java.__set__('execa', async () => ({ stdout: '/tmp/jdk' })); + + const env = {}; + + await Java._ensure(env); + + expect(fsSpy).toHaveBeenCalledWith('/usr/libexec/java_home'); + expect(env.JAVA_HOME).toBe('/tmp/jdk'); + }); + + it('without JAVA_HOME / with javac - Mac OS X - error', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin'); + Java.__set__('fs', { existsSync: () => true }); + Java.__set__('execa', jasmine.createSpy('execa').and.returnValue(Promise.reject({ + shortMessage: 'test error' + }))); + const emitSpy = jasmine.createSpy('events.emit'); + Java.__set__('events', { + emit: emitSpy + }); + + await expectAsync(Java._ensure({})) + .toBeRejectedWithError(CordovaError, /Failed to find 'JAVA_HOME' environment variable/); + + expect(emitSpy).toHaveBeenCalledWith('verbose', 'test error'); + }); + + it('derive from javac location - success', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin'); + Java.__set__('fs', { existsSync: path => !/java_home$/.test(path) }); + const env = {}; + await Java._ensure(env); + expect(env.JAVA_HOME).toBe('/tmp'); + }); + + it('derive from javac location - error', async () => { + spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin'); + Java.__set__('fs', { existsSync: () => false }); + await expectAsync(Java._ensure({})).toBeRejectedWithError(CordovaError, /Failed to find 'JAVA_HOME' environment variable/); + }); + }); +});