Skip to content

Commit c601915

Browse files
feat: force doctor check when tns doctor is executed
In case `tns doctor` is called, force the check of the environment requirements. In case an issue is detected, remove the file with cached information for all projects - there seems to be issue with the configuration, so we need to check all projects again.
1 parent 66076e3 commit c601915

File tree

6 files changed

+187
-23
lines changed

6 files changed

+187
-23
lines changed

lib/common/commands/doctor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export class DoctorCommand implements ICommand {
66
public allowedParameters: ICommandParameter[] = [];
77

88
public execute(args: string[]): Promise<void> {
9-
return this.$doctorService.printWarnings({ trackResult: false, projectDir: this.$projectHelper.projectDir });
9+
return this.$doctorService.printWarnings({ trackResult: false, projectDir: this.$projectHelper.projectDir, forceCheck: true });
1010
}
1111
}
1212

lib/common/declarations.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ interface IDoctorService {
11951195
* @param configOptions: defines if the result should be tracked by Analytics
11961196
* @returns {Promise<void>}
11971197
*/
1198-
printWarnings(configOptions?: { trackResult: boolean, projectDir?: string, runtimeVersion?: string, options?: IOptions }): Promise<void>;
1198+
printWarnings(configOptions?: { trackResult?: boolean, projectDir?: string, runtimeVersion?: string, options?: IOptions, forceCheck?: boolean }): Promise<void>;
11991199
/**
12001200
* Runs the setup script on host machine
12011201
* @returns {Promise<ISpawnResult>}
@@ -1206,7 +1206,7 @@ interface IDoctorService {
12061206
* @param platform @optional The current platform
12071207
* @returns {Promise<boolean>} true if the environment is properly configured for local builds
12081208
*/
1209-
canExecuteLocalBuild(platform?: string, projectDir?: string, runtimeVersion?: string): Promise<boolean>;
1209+
canExecuteLocalBuild(configuration?: { platform?: string, projectDir?: string, runtimeVersion?: string, forceCheck?: boolean }): Promise<boolean>;
12101210

12111211
/**
12121212
* Checks and notifies users for deprecated short imports in their applications.

lib/definitions/platform.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ interface ICheckEnvironmentRequirementsInput {
8080
runtimeVersion?: string;
8181
options?: IOptions;
8282
notConfiguredEnvOptions?: INotConfiguredEnvOptions;
83+
forceCheck?: boolean;
8384
}
8485

8586
interface ICheckEnvironmentRequirementsOutput {

lib/services/doctor-service.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EOL } from "os";
22
import * as path from "path";
33
import * as helpers from "../common/helpers";
4+
import { cache } from "../common/decorators";
45
import { TrackActionNames, NODE_MODULES_FOLDER_NAME, TNS_CORE_MODULES_NAME } from "../constants";
56
import { doctor, constants } from "nativescript-doctor";
67

@@ -9,9 +10,14 @@ export class DoctorService implements IDoctorService {
910
private static WindowsSetupScriptExecutable = "powershell.exe";
1011
private static WindowsSetupScriptArguments = ["start-process", "-FilePath", "PowerShell.exe", "-NoNewWindow", "-Wait", "-ArgumentList", '"-NoProfile -ExecutionPolicy Bypass -Command iex ((new-object net.webclient).DownloadString(\'https://www.nativescript.org/setup/win\'))"'];
1112

13+
@cache()
14+
private get jsonFileSettingsPath(): string {
15+
return path.join(this.$settingsService.getProfileDir(), "doctor-cache.json");
16+
}
17+
18+
@cache()
1219
private get $jsonFileSettingsService(): IJsonFileSettingsService {
13-
const jsonFileSettingsPath = path.join(this.$settingsService.getProfileDir(), "doctor-cache.json");
14-
return this.$injector.resolve<IJsonFileSettingsService>("jsonFileSettingsService", { jsonFileSettingsPath });
20+
return this.$injector.resolve<IJsonFileSettingsService>("jsonFileSettingsService", { jsonFileSettingsPath: this.jsonFileSettingsPath });
1521
}
1622

1723
constructor(private $analyticsService: IAnalyticsService,
@@ -25,11 +31,12 @@ export class DoctorService implements IDoctorService {
2531
private $versionsService: IVersionsService,
2632
private $settingsService: ISettingsService) { }
2733

28-
public async printWarnings(configOptions?: { trackResult: boolean, projectDir?: string, runtimeVersion?: string, options?: IOptions }): Promise<void> {
29-
const getInfosData: any = { projectDir: configOptions && configOptions.projectDir, androidRuntimeVersion: configOptions && configOptions.runtimeVersion };
34+
public async printWarnings(configOptions?: { trackResult: boolean, projectDir?: string, runtimeVersion?: string, options?: IOptions, forceCheck?: boolean }): Promise<void> {
35+
configOptions = configOptions || <any>{};
36+
const getInfosData: any = { projectDir: configOptions.projectDir, androidRuntimeVersion: configOptions.runtimeVersion };
3037
const infos = await this.$terminalSpinnerService.execute<NativeScriptDoctor.IInfo[]>({
3138
text: `Getting environment information ${EOL}`
32-
}, () => this.getInfos(getInfosData));
39+
}, () => this.getInfos({ forceCheck: configOptions.forceCheck }, getInfosData));
3340

3441
const warnings = infos.filter(info => info.type === constants.WARNING_TYPE_NAME);
3542
const hasWarnings = warnings.length > 0;
@@ -46,6 +53,9 @@ export class DoctorService implements IDoctorService {
4653

4754
if (hasWarnings) {
4855
this.$logger.info("There seem to be issues with your configuration.");
56+
// cleanup the cache file as there seems to be issues with the current config
57+
// all projects need to be rechecked
58+
this.$fs.deleteFile(this.jsonFileSettingsPath);
4959
} else {
5060
this.$logger.info("No issues were detected.".bold);
5161
await this.$jsonFileSettingsService.saveSetting(this.getKeyForConfiguration(getInfosData), infos);
@@ -62,9 +72,9 @@ export class DoctorService implements IDoctorService {
6272

6373
await this.$injector.resolve<IPlatformEnvironmentRequirements>("platformEnvironmentRequirements").checkEnvironmentRequirements({
6474
platform: null,
65-
projectDir: configOptions && configOptions.projectDir,
66-
runtimeVersion: configOptions && configOptions.runtimeVersion,
67-
options: configOptions && configOptions.options
75+
projectDir: configOptions.projectDir,
76+
runtimeVersion: configOptions.runtimeVersion,
77+
options: configOptions.options
6878
});
6979
}
7080

@@ -98,16 +108,19 @@ export class DoctorService implements IDoctorService {
98108
});
99109
}
100110

101-
public async canExecuteLocalBuild(platform?: string, projectDir?: string, runtimeVersion?: string): Promise<boolean> {
111+
public async canExecuteLocalBuild(configuration?: { platform?: string, projectDir?: string, runtimeVersion?: string, forceCheck?: boolean }): Promise<boolean> {
102112
await this.$analyticsService.trackEventActionInGoogleAnalytics({
103113
action: TrackActionNames.CheckLocalBuildSetup,
104114
additionalData: "Starting",
105115
});
106-
const sysInfoConfig: NativeScriptDoctor.ISysInfoConfig = { platform, projectDir, androidRuntimeVersion: runtimeVersion };
107-
const infos = await this.getInfos(sysInfoConfig);
116+
const sysInfoConfig: NativeScriptDoctor.ISysInfoConfig = { platform: configuration.platform, projectDir: configuration.projectDir, androidRuntimeVersion: configuration.runtimeVersion };
117+
const infos = await this.getInfos({ forceCheck: configuration && configuration.forceCheck }, sysInfoConfig);
108118
const warnings = this.filterInfosByType(infos, constants.WARNING_TYPE_NAME);
109119
const hasWarnings = warnings.length > 0;
110120
if (hasWarnings) {
121+
// cleanup the cache file as there seems to be issues with the current config
122+
// all projects need to be rechecked
123+
this.$fs.deleteFile(this.jsonFileSettingsPath);
111124
await this.$analyticsService.trackEventActionInGoogleAnalytics({
112125
action: TrackActionNames.CheckLocalBuildSetup,
113126
additionalData: `Warnings:${warnings.map(w => w.message).join("__")}`,
@@ -252,10 +265,15 @@ export class DoctorService implements IDoctorService {
252265
return data;
253266
}
254267

255-
private async getInfos(sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig): Promise<NativeScriptDoctor.IInfo[]> {
268+
private async getInfos(cacheConfig: { forceCheck: boolean }, sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig): Promise<NativeScriptDoctor.IInfo[]> {
256269
const key = this.getKeyForConfiguration(sysInfoConfig);
257-
// check if we already have cache for the results here
258-
const infosFromCache = await this.$jsonFileSettingsService.getSettingValue<NativeScriptDoctor.IInfo[]>(key);
270+
271+
const infosFromCache = cacheConfig.forceCheck ?
272+
null :
273+
await this.$jsonFileSettingsService.getSettingValue<NativeScriptDoctor.IInfo[]>(key);
274+
275+
this.$logger.trace(`getInfos cacheConfig options:`, cacheConfig, " current info from cache: ", infosFromCache);
276+
259277
const infos = infosFromCache || await doctor.getInfos(sysInfoConfig);
260278

261279
return infos;

lib/services/platform-environment-requirements.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ
5757
};
5858
}
5959

60-
const canExecute = await this.$doctorService.canExecuteLocalBuild(platform, projectDir, runtimeVersion);
60+
const canExecute = await this.$doctorService.canExecuteLocalBuild({ platform, projectDir, runtimeVersion, forceCheck: input.forceCheck });
61+
6162
if (!canExecute) {
6263
if (!isInteractive()) {
6364
await this.$analyticsService.trackEventActionInGoogleAnalytics({
@@ -80,7 +81,7 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ
8081
if (selectedOption === PlatformEnvironmentRequirements.LOCAL_SETUP_OPTION_NAME) {
8182
await this.$doctorService.runSetupScript();
8283

83-
if (await this.$doctorService.canExecuteLocalBuild(platform, projectDir, runtimeVersion)) {
84+
if (await this.$doctorService.canExecuteLocalBuild({ platform, projectDir, runtimeVersion, forceCheck: input.forceCheck })) {
8485
return {
8586
canExecute: true,
8687
selectedOption
@@ -114,7 +115,7 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ
114115

115116
if (selectedOption === PlatformEnvironmentRequirements.BOTH_CLOUD_SETUP_AND_LOCAL_SETUP_OPTION_NAME) {
116117
await this.processBothCloudBuildsAndSetupScript();
117-
if (await this.$doctorService.canExecuteLocalBuild(platform, projectDir, runtimeVersion)) {
118+
if (await this.$doctorService.canExecuteLocalBuild({ platform, projectDir, runtimeVersion, forceCheck: input.forceCheck })) {
118119
return {
119120
canExecute: true,
120121
selectedOption

test/services/doctor-service.ts

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { DoctorService } from "../../lib/services/doctor-service";
22
import { Yok } from "../../lib/common/yok";
3-
import { LoggerStub } from "../stubs";
3+
import { LoggerStub, FileSystemStub } from "../stubs";
44
import { assert } from "chai";
55
import * as path from "path";
6+
import * as sinon from "sinon";
7+
const nativescriptDoctor = require("nativescript-doctor");
68

79
class DoctorServiceInheritor extends DoctorService {
810
constructor($analyticsService: IAnalyticsService,
@@ -32,8 +34,15 @@ describe("doctorService", () => {
3234
testInjector.register("logger", LoggerStub);
3335
testInjector.register("childProcess", {});
3436
testInjector.register("projectDataService", {});
35-
testInjector.register("fs", {});
36-
testInjector.register("terminalSpinnerService", {});
37+
testInjector.register("fs", FileSystemStub);
38+
testInjector.register("terminalSpinnerService", {
39+
execute: (spinnerOptions: ITerminalSpinnerOptions, action: () => Promise<any>): Promise<any> => action(),
40+
createSpinner: (spinnerOptions?: ITerminalSpinnerOptions): ITerminalSpinner => (<any>{
41+
text: '',
42+
succeed: (): any => undefined,
43+
fail: (): any => undefined
44+
})
45+
});
3746
testInjector.register("versionsService", {});
3847
testInjector.register("settingsService", {
3948
getProfileDir: (): string => ""
@@ -42,6 +51,9 @@ describe("doctorService", () => {
4251
getSettingValue: async (settingName: string, cacheOpts?: ICacheTimeoutOpts): Promise<any> => undefined,
4352
saveSetting: async (key: string, value: any, cacheOpts?: IUseCacheOpts): Promise<void> => undefined
4453
});
54+
testInjector.register("platformEnvironmentRequirements", {
55+
checkEnvironmentRequirements: async (input: ICheckEnvironmentRequirementsInput): Promise<ICheckEnvironmentRequirementsOutput> => (<any>{})
56+
});
4557

4658
return testInjector;
4759
};
@@ -262,4 +274,136 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl
262274
});
263275
});
264276
});
277+
278+
describe("printWarnings", () => {
279+
let sandbox: sinon.SinonSandbox;
280+
281+
beforeEach(() => {
282+
sandbox = sinon.sandbox.create();
283+
});
284+
285+
afterEach(() => {
286+
sandbox.restore();
287+
});
288+
const successGetInfosResult = [{
289+
message:
290+
'Your ANDROID_HOME environment variable is set and points to correct directory.',
291+
platforms: ['Android'],
292+
type: 'info'
293+
},
294+
{
295+
message: 'Xcode is installed and is configured properly.',
296+
platforms: ['iOS'],
297+
type: 'info'
298+
}];
299+
300+
const failedGetInfosResult = [{
301+
message:
302+
'The ANDROID_HOME environment variable is not set or it points to a non-existent directory. You will not be able to perform any build-related operations for Android.',
303+
additionalInformation:
304+
'To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory.',
305+
platforms: ['Android'],
306+
type: 'warning'
307+
},
308+
{
309+
message:
310+
'WARNING: adb from the Android SDK is not installed or is not configured properly. ',
311+
additionalInformation:
312+
'For Android-related operations, the NativeScript CLI will use a built-in version of adb.\nTo avoid possible issues with the native Android emulator, Genymotion or connected\nAndroid devices, verify that you have installed the latest Android SDK and\nits dependencies as described in http://developer.android.com/sdk/index.html#Requirements',
313+
platforms: ['Android'],
314+
type: 'warning'
315+
},
316+
{
317+
message: 'Xcode is installed and is configured properly.',
318+
platforms: ['iOS'],
319+
type: 'info'
320+
}];
321+
322+
it("prints correct message when no issues are detected", async () => {
323+
const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos");
324+
nsDoctorStub.returns(successGetInfosResult);
325+
const testInjector = createTestInjector();
326+
const doctorService = testInjector.resolve<IDoctorService>("doctorService");
327+
const logger = testInjector.resolve<LoggerStub>("logger");
328+
await doctorService.printWarnings();
329+
assert.isTrue(logger.output.indexOf("No issues were detected.") !== -1);
330+
});
331+
332+
it("prints correct message when issues are detected", async () => {
333+
const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos");
334+
nsDoctorStub.returns(failedGetInfosResult);
335+
const testInjector = createTestInjector();
336+
const doctorService = testInjector.resolve<IDoctorService>("doctorService");
337+
const logger = testInjector.resolve<LoggerStub>("logger");
338+
await doctorService.printWarnings();
339+
assert.isTrue(logger.output.indexOf("There seem to be issues with your configuration.") !== -1);
340+
});
341+
342+
it("returns result from cached file when they exist and the forceCheck is not passed", async () => {
343+
const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos");
344+
nsDoctorStub.throws(new Error("We should not call nativescript-doctor package when we have results in the file."));
345+
346+
const testInjector = createTestInjector();
347+
const doctorService = testInjector.resolve<IDoctorService>("doctorService");
348+
const jsonFileSettingsService = testInjector.resolve<IJsonFileSettingsService>("jsonFileSettingsService");
349+
jsonFileSettingsService.getSettingValue = async (settingName: string, cacheOpts?: ICacheTimeoutOpts): Promise<any> => successGetInfosResult;
350+
let saveSettingValue: any = null;
351+
jsonFileSettingsService.saveSetting = async (key: string, value: any, cacheOpts?: IUseCacheOpts): Promise<void> => saveSettingValue = value;
352+
const logger = testInjector.resolve<LoggerStub>("logger");
353+
await doctorService.printWarnings();
354+
assert.isTrue(logger.output.indexOf("No issues were detected.") !== -1);
355+
assert.deepEqual(saveSettingValue, successGetInfosResult);
356+
});
357+
358+
it("saves results in cache when there are no warnings", async () => {
359+
const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos");
360+
nsDoctorStub.returns(successGetInfosResult);
361+
362+
const testInjector = createTestInjector();
363+
const doctorService = testInjector.resolve<IDoctorService>("doctorService");
364+
const jsonFileSettingsService = testInjector.resolve<IJsonFileSettingsService>("jsonFileSettingsService");
365+
let saveSettingValue: any = null;
366+
jsonFileSettingsService.saveSetting = async (key: string, value: any, cacheOpts?: IUseCacheOpts): Promise<void> => saveSettingValue = value;
367+
const logger = testInjector.resolve<LoggerStub>("logger");
368+
await doctorService.printWarnings();
369+
assert.isTrue(logger.output.indexOf("No issues were detected.") !== -1);
370+
assert.deepEqual(saveSettingValue, successGetInfosResult);
371+
});
372+
373+
it("returns result from nativescript-doctor and saves them in cache when the forceCheck is passed", async () => {
374+
const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos");
375+
nsDoctorStub.returns(successGetInfosResult);
376+
377+
const testInjector = createTestInjector();
378+
const doctorService = testInjector.resolve<IDoctorService>("doctorService");
379+
const jsonFileSettingsService = testInjector.resolve<IJsonFileSettingsService>("jsonFileSettingsService");
380+
let saveSettingValue: any = null;
381+
let isGetSettingValueCalled = false;
382+
jsonFileSettingsService.getSettingValue = async (settingName: string, cacheOpts?: ICacheTimeoutOpts): Promise<any> => {
383+
isGetSettingValueCalled = true;
384+
return null;
385+
};
386+
jsonFileSettingsService.saveSetting = async (key: string, value: any, cacheOpts?: IUseCacheOpts): Promise<void> => saveSettingValue = value;
387+
const logger = testInjector.resolve<LoggerStub>("logger");
388+
await doctorService.printWarnings({ forceCheck: true });
389+
assert.isTrue(logger.output.indexOf("No issues were detected.") !== -1);
390+
assert.deepEqual(saveSettingValue, successGetInfosResult);
391+
assert.isTrue(nsDoctorStub.calledOnce);
392+
assert.isFalse(isGetSettingValueCalled, "When forceCheck is passed, we should not read the cache file.");
393+
});
394+
395+
it("deletes the cache file when issues are detected", async () => {
396+
const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos");
397+
nsDoctorStub.returns(failedGetInfosResult);
398+
const testInjector = createTestInjector();
399+
const doctorService = testInjector.resolve<IDoctorService>("doctorService");
400+
const fs = testInjector.resolve<IFileSystem>("fs");
401+
let deletedPath = "";
402+
fs.deleteFile = (filePath: string): void => {deletedPath = filePath; };
403+
const logger = testInjector.resolve<LoggerStub>("logger");
404+
await doctorService.printWarnings();
405+
assert.isTrue(logger.output.indexOf("There seem to be issues with your configuration.") !== -1);
406+
assert.isTrue(deletedPath.indexOf("doctor-cache.json") !== -1);
407+
});
408+
});
265409
});

0 commit comments

Comments
 (0)