Handle the case where sfdx binary is not installed (#8)

Also, enforced "ordered-imports": true and reordered imports.

@W-4146476@
This commit is contained in:
Nick Chen 2017-07-14 13:46:52 -07:00 коммит произвёл GitHub
Родитель 85a7b3babb
Коммит 2fc1287252
26 изменённых файлов: 299 добавлений и 168 удалений

19
.vscode/launch.json поставляемый
Просмотреть файл

@ -21,6 +21,25 @@
"outFiles": ["${workspaceRoot}/packages/*/out/src/**/*.js"],
"preLaunchTask": "Compile"
},
{
"type": "node",
"request": "launch",
"name": "Launch SalesforceDX Utils Tests",
"program": "${workspaceRoot}/packages/salesforcedx-utils-vscode/node_modules/mocha/bin/_mocha",
"args": [
"-u",
"tdd",
"--timeout",
"999999",
"--colors",
"--recursive",
"${workspaceRoot}/packages/salesforcedx-utils-vscode/out/test"
],
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/packages/*/out/**/*.js"],
"preLaunchTask": "Compile",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Launch SalesforceDX VS Code Core Tests",
"type": "extensionHost",

Просмотреть файл

@ -1,8 +1,8 @@
import { spawn, ChildProcess, ExecOptions } from 'child_process';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { ChildProcess, ExecOptions, spawn } from 'child_process';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/observable/interval';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
// tslint:disable-next-line:no-var-requires
const kill = require('tree-kill');
@ -41,7 +41,8 @@ export class CliCommandExecutor {
export class CommandExecution {
public readonly command: Command;
public readonly cancellationToken?: CancellationToken;
public readonly processExitSubject: Observable<number | string | null>;
public readonly processExitSubject: Observable<number | undefined>;
public readonly processErrorSubject: Observable<Error | undefined>;
public readonly stdoutSubject: Observable<Buffer | string>;
public readonly stderrSubject: Observable<Buffer | string>;
@ -59,7 +60,16 @@ export class CommandExecution {
this.processExitSubject = Observable.fromEvent(
childProcess,
'exit'
) as Observable<number | string | null>;
) as Observable<number | undefined>;
this.processExitSubject.subscribe(next => {
if (timerSubscriber) {
timerSubscriber.unsubscribe();
}
});
this.processErrorSubject = Observable.fromEvent(
childProcess,
'error'
) as Observable<Error | undefined>;
this.processExitSubject.subscribe(next => {
if (timerSubscriber) {
timerSubscriber.unsubscribe();

Просмотреть файл

@ -1,6 +1,10 @@
import { expect } from 'chai';
import { CliCommandExecutor, SfdxCommandBuilder } from '../../src/cli';
import {
CliCommandExecutor,
CommandBuilder,
SfdxCommandBuilder
} from '../../src/cli';
describe('CommandExecutor tests', () => {
describe('Handle listeners on stdout and stderr', () => {
@ -17,7 +21,7 @@ describe('CommandExecutor tests', () => {
const exitCode = await new Promise<string>((resolve, reject) => {
execution.processExitSubject.subscribe(
data => {
resolve(data !== null ? data.toString() : '');
resolve(data != undefined ? data.toString() : '');
},
err => {
reject(err);
@ -45,7 +49,7 @@ describe('CommandExecutor tests', () => {
const exitCode = await new Promise<string>((resolve, reject) => {
execution.processExitSubject.subscribe(
data => {
resolve(data !== null ? data.toString() : '');
resolve(data != undefined ? data.toString() : '');
},
err => {
reject(err);
@ -58,4 +62,26 @@ describe('CommandExecutor tests', () => {
expect(stderr).to.contain('Error: Unexpected flag --unknown');
});
});
describe('Handle listeners on error', () => {
it('Should relay error event', async () => {
const execution = new CliCommandExecutor(
new CommandBuilder('bogus').build(),
{}
).execute();
const errorData = await new Promise<string>((resolve, reject) => {
execution.processErrorSubject.subscribe(
data => {
resolve(data != undefined ? data.toString() : '');
},
err => {
reject(err);
}
);
});
expect(errorData).to.equal('Error: spawn bogus ENOENT');
});
});
});

Просмотреть файл

@ -1,14 +1,14 @@
import * as vscode from 'vscode';
import * as child_process from 'child_process';
import * as path from 'path';
import * as net from 'net';
import * as path from 'path';
import * as portFinder from 'portfinder';
import * as requirements from './requirements';
import * as vscode from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
StreamInfo
} from 'vscode-languageclient';
import * as requirements from './requirements';
const UBER_JAR_NAME = 'apex-jorje-lsp.jar';
const JDWP_DEBUG_PORT = 2739;

Просмотреть файл

@ -1,8 +1,8 @@
// From https://github.com/redhat-developer/vscode-java
// Original version licensed under the Eclipse Public License (EPL)
import { workspace } from 'vscode';
import * as cp from 'child_process';
import { workspace } from 'vscode';
import pathExists = require('path-exists');

Просмотреть файл

@ -37,7 +37,7 @@ export class ChannelService {
execution.processExitSubject.subscribe(data => {
this.channel.append(execution.command.toString());
this.channel.append(' ');
if (data !== null) {
if (data != undefined) {
this.channel.appendLine(
localize('channel_end_with_exit_code', data.toString())
);
@ -45,5 +45,21 @@ export class ChannelService {
this.channel.appendLine(localize('channel_end'));
}
});
execution.processErrorSubject.subscribe(data => {
this.channel.append(execution.command.toString());
this.channel.append(' ');
if (data != undefined) {
this.channel.appendLine(
localize('channel_end_with_error', data.toString())
);
if (/sfdx.*ENOENT/.test(data.message)) {
this.channel.appendLine(localize('channel_end_with_sfdx_not_found'));
}
} else {
this.channel.appendLine(localize('channel_end'));
}
});
}
}

Просмотреть файл

@ -1,9 +1,9 @@
import * as path from 'path';
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as path from 'path';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,9 +1,9 @@
import * as path from 'path';
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as path from 'path';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import {
CliCommandExecutor,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as vscode from 'vscode';
import { channelService } from '../channels';
import { notificationService } from '../notifications';
import { CancellableStatusBar, taskViewService } from '../statuses';

Просмотреть файл

@ -1,7 +1,5 @@
import * as vscode from 'vscode';
import * as scratchOrgDecorator from './scratch-org-decorator';
import { CANCEL_EXECUTION_COMMAND, cancelCommandExecution } from './statuses';
import {
forceApexTestRun,
forceAuthWebLogin,
@ -12,6 +10,8 @@ import {
forceSourceStatus,
forceTaskStop
} from './commands';
import * as scratchOrgDecorator from './scratch-org-decorator';
import { CANCEL_EXECUTION_COMMAND, cancelCommandExecution } from './statuses';
import { taskViewService } from './statuses';
function registerCommands(): vscode.Disposable {

Просмотреть файл

@ -47,6 +47,9 @@ const messages: Messages = {
channel_name: 'SalesforceDX CLI',
channel_starting_message: 'Starting ',
channel_end_with_exit_code: 'ended with exit code %s',
channel_end_with_sfdx_not_found:
'The SFDX CLI is not installed. Install it from https://developer.salesforce.com/tools/sfdxcli',
channel_end_with_error: 'ended with error %s',
channel_end: 'ended',
notification_successful_execution_message: 'Successfully executed %s',

Просмотреть файл

@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { Observable } from 'rxjs/Observable';
import { CommandExecution } from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { Observable } from 'rxjs/Observable';
import * as vscode from 'vscode';
import { DEFAULT_SFDX_CHANNEL } from '../channels';
import { localize } from '../messages';
@ -26,20 +26,22 @@ export class NotificationService {
// https://stackoverflow.com/questions/38168581/observablet-is-not-a-class-derived-from-observablet
this.reportExecutionStatus(
execution.command.toString(),
(execution.processExitSubject as any) as Observable<
number | string | null
>,
(execution.processExitSubject as any) as Observable<number | undefined>,
cancellationToken
);
this.reportExecutionError(
execution.command.toString(),
(execution.processErrorSubject as any) as Observable<Error | undefined>
);
}
public reportExecutionStatus(
executionName: string,
observable: Observable<number | string | null>,
observable: Observable<number | undefined>,
cancellationToken?: vscode.CancellationToken
) {
observable.subscribe(async data => {
if (data !== null && data.toString() === '0') {
if (data != undefined && data.toString() === '0') {
const showButtonText = localize('notification_show_button_text');
const selection = await vscode.window.showInformationMessage(
localize('notification_successful_execution_message', executionName),
@ -66,4 +68,16 @@ export class NotificationService {
}
});
}
public reportExecutionError(
executionName: string,
observable: Observable<Error | undefined>
) {
observable.subscribe(async data => {
vscode.window.showErrorMessage(
localize('notification_unsuccessful_execution_message', executionName)
);
this.channel.show();
});
}
}

Просмотреть файл

@ -1,6 +1,6 @@
import { window, workspace, StatusBarItem, StatusBarAlignment } from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as path from 'path';
import { StatusBarAlignment, StatusBarItem, window, workspace } from 'vscode';
const CONFIG_FILE = path.join(workspace.rootPath!, '.sfdx/sfdx-config.json');

Просмотреть файл

@ -1,10 +1,10 @@
import { CommandExecution } from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import {
CancellationTokenSource,
StatusBarItem,
StatusBarAlignment,
StatusBarItem,
window
} from 'vscode';
import { CommandExecution } from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { localize } from '../../src/messages';
export const CANCEL_EXECUTION_COMMAND = 'internal.cancel.execution.command';
@ -56,6 +56,12 @@ export class CancellableStatusBar {
}
resetStatusBarItem();
});
execution.processErrorSubject.subscribe(data => {
if (statusTimer) {
clearInterval(statusTimer);
}
resetStatusBarItem();
});
statusBarItem.show();
}

Просмотреть файл

@ -1,3 +1,4 @@
import { CommandExecution } from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import {
CancellationTokenSource,
Event,
@ -6,7 +7,6 @@ import {
TreeItem,
TreeItemCollapsibleState
} from 'vscode';
import { CommandExecution } from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { localize } from '../messages';
export class TaskViewService implements TreeDataProvider<Task> {
@ -107,6 +107,9 @@ export class Task extends TreeItem {
this.execution.processExitSubject.subscribe(data => {
this.taskViewProvider.removeTask(this);
});
this.execution.processErrorSubject.subscribe(data => {
this.taskViewProvider.removeTask(this);
});
}
public cancel(): boolean {

Просмотреть файл

@ -1,14 +1,16 @@
import { expect } from 'chai';
import { OutputChannel, ViewColumn } from 'vscode';
import { EOL } from 'os';
import {
CliCommandExecutor,
CommandBuilder,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { expect } from 'chai';
import { EOL } from 'os';
import { OutputChannel, ViewColumn } from 'vscode';
import {
DEFAULT_SFDX_CHANNEL,
ChannelService
ChannelService,
DEFAULT_SFDX_CHANNEL
} from '../../src/channels/channelService';
import { localize } from '../../src/messages';
class MockChannel implements OutputChannel {
public readonly name = 'MockChannel';
@ -23,7 +25,7 @@ class MockChannel implements OutputChannel {
this.value += EOL;
}
// These methods are not mocked or needed
// These methods are not mocked but needed as part of the interface
// tslint:disable:no-empty
public clear(): void {}
public show(preserveFocus?: boolean | undefined): void;
@ -86,5 +88,26 @@ describe('Channel', () => {
expect(mChannel.value).to.contain('Error: Unexpected flag --unknown');
expect(mChannel.value).to.contain('ended with exit code 2');
});
it('Should suggest to install SFDX binary', async () => {
const execution = new CliCommandExecutor(
new CommandBuilder('sfdx_')
.withArg('force')
.withArg('--unknown')
.build(),
{}
).execute();
channelService.streamCommandOutput(execution);
await new Promise<string>((resolve, reject) => {
execution.processErrorSubject.subscribe(data => {
resolve();
});
});
expect(mChannel.value).to.contain(
localize('channel_end_with_sfdx_not_found')
);
});
});
});

Просмотреть файл

@ -1,9 +1,9 @@
import { SinonStub, assert, stub } from 'sinon';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { assert, SinonStub, stub } from 'sinon';
import { CancellationTokenSource, window } from 'vscode';
import { NotificationService } from '../../src/notifications/notificationService';
import { DEFAULT_SFDX_CHANNEL } from '../../src/channels/channelService';
import { localize } from '../../src/messages';
import { NotificationService } from '../../src/notifications/notificationService';
const SHOW_BUTTON_TEXT = localize('notification_show_button_text');
@ -35,7 +35,7 @@ describe('Notifications', () => {
});
it('Should notify successful execution', async () => {
const observable = new ReplaySubject<number | string | null>();
const observable = new ReplaySubject<number | undefined>();
observable.next(0);
const notificationService = NotificationService.getInstance();
@ -57,7 +57,7 @@ describe('Notifications', () => {
mShowInformation = stub(window, 'showInformationMessage').returns(
Promise.resolve(SHOW_BUTTON_TEXT)
);
const observable = new ReplaySubject<number | string | null>();
const observable = new ReplaySubject<number | undefined>();
observable.next(0);
const notificationService = NotificationService.getInstance();
@ -74,8 +74,8 @@ describe('Notifications', () => {
});
it('Should notify cancellation', async () => {
const observable = new ReplaySubject<number | string | null>();
observable.next(null);
const observable = new ReplaySubject<number | undefined>();
observable.next(undefined);
const cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.cancel();
@ -94,7 +94,7 @@ describe('Notifications', () => {
it('Should notify unsuccessful execution', async () => {
const ABNORMAL_EXIT = -1;
const observable = new ReplaySubject<number | string | null>();
const observable = new ReplaySubject<number | undefined>();
observable.next(ABNORMAL_EXIT);
const notificationService = NotificationService.getInstance();
@ -105,4 +105,18 @@ describe('Notifications', () => {
assert.notCalled(mShowWarningMessage);
assert.calledWith(mShowErrorMessage, 'Failed to execute mock command');
});
it('Should notify errorneous execution', async () => {
const error = new Error('');
const observable = new ReplaySubject<Error | undefined>();
observable.next(error);
const notificationService = NotificationService.getInstance();
await notificationService.reportExecutionError('mock command', observable);
assert.calledOnce(mShow);
assert.notCalled(mShowInformation);
assert.notCalled(mShowWarningMessage);
assert.calledWith(mShowErrorMessage, 'Failed to execute mock command');
});
});

Просмотреть файл

@ -1,22 +1,22 @@
// tslint:disable:no-unused-expression
import {
CliCommandExecutor,
CommandExecution,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { expect } from 'chai';
import { CancellationTokenSource, StatusBarItem } from 'vscode';
import {
SfdxCommandBuilder,
CliCommandExecutor,
CommandExecution
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { localize } from '../../src/messages';
import {
CANCEL_EXECUTION_COMMAND,
CancellableStatusBar,
cancelCommandExecution,
CancellableStatusBar,
cancellationTokenSource,
cycleStatusBarText,
statusBarItem,
statusTimer
} from '../../src/statuses/statusBar';
import { localize } from '../../src/messages';
describe('Status Bar', () => {
let tokenSource: CancellationTokenSource;

Просмотреть файл

@ -1,14 +1,15 @@
// tslint:disable:no-unused-expression
import { stub } from 'sinon';
import { expect } from 'chai';
import { CancellationTokenSource } from 'vscode';
import {
SfdxCommandBuilder,
CliCommandExecutor
CliCommandExecutor,
CommandBuilder,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { Task, TaskViewService } from '../../src/statuses/taskView';
import { expect } from 'chai';
import { stub } from 'sinon';
import { CancellationTokenSource } from 'vscode';
import { localize } from '../../src/messages';
import { Task, TaskViewService } from '../../src/statuses/taskView';
describe('Task View', () => {
describe('Task View Provider', () => {
@ -137,5 +138,24 @@ describe('Task View', () => {
expect(taskViewService.getChildren()).to.have.lengthOf(0);
});
it('Should remove itself from Task View if erroneous', async () => {
const taskViewService = new TaskViewService();
const execution = new CliCommandExecutor(
new CommandBuilder('sfdx_').build(),
{}
).execute();
taskViewService.addCommandExecution(execution);
expect(taskViewService.getChildren()).to.have.lengthOf(1);
await new Promise((resolve, reject) => {
taskViewService.onDidChangeTreeData(e => {
resolve(e);
});
});
expect(taskViewService.getChildren()).to.have.lengthOf(0);
});
});
});

Просмотреть файл

@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { expect } from 'chai';
import * as vscode from 'vscode';
const PERFECT_MATCH = 10;

Просмотреть файл

@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { expect } from 'chai';
import * as vscode from 'vscode';
const PERFECT_MATCH = 10;

Просмотреть файл

@ -1,106 +1,83 @@
// From https://github.com/palantir/tslint/blob/master/docs/sample.tslint.json
{
"rules": {
"class-name": true,
"curly": true,
"forin": true,
"jsdoc-format": true,
"label-position": true,
"member-access": true,
"new-parens": true,
"no-any": false,
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-internal-module": true,
"no-invalid-this": [
true,
"check-function-in-method"
],
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-unused-variable": true,
"no-var-keyword": true,
"no-var-requires": true,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-finally",
"check-whitespace"
],
"prefer-const": [
true,
{
"destructuring": "all"
}
],
"quotemark": [
true,
"single",
"avoid-escape"
],
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check",
"allow-undefined-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "space",
"index-signature": "space",
"parameter": "space",
"property-declaration": "space",
"variable-declaration": "space"
}
],
"use-isnan": true,
"variable-name": [
true,
"check-format",
"allow-leading-underscore",
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
"rules": {
"class-name": true,
"curly": true,
"forin": true,
"jsdoc-format": true,
"label-position": true,
"member-access": true,
"new-parens": true,
"no-any": false,
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": false,
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-inferrable-types": [true, "ignore-params"],
"no-internal-module": true,
"no-invalid-this": [true, "check-function-in-method"],
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-unused-variable": true,
"no-var-keyword": true,
"no-var-requires": true,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-finally",
"check-whitespace"
],
"ordered-imports": [true],
"prefer-const": [
true,
{
"destructuring": "all"
}
],
"quotemark": [true, "single", "avoid-escape"],
"semicolon": [true, "always"],
"triple-equals": [true, "allow-null-check", "allow-undefined-check"],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "space",
"index-signature": "space",
"parameter": "space",
"property-declaration": "space",
"variable-declaration": "space"
}
],
"use-isnan": true,
"variable-name": [
true,
"check-format",
"allow-leading-underscore",
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}