Skip to content

Commit

Permalink
Merge branch 'master' into ben/electron-sandbox
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed May 26, 2020
2 parents 3c4820c + 66ef23d commit 76590a7
Show file tree
Hide file tree
Showing 36 changed files with 509 additions and 176 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
"vs/nls",
"**/vs/base/common/**",
"**/vs/base/parts/*/common/**",
"**/vs/base/test/common/**",
"**/vs/platform/*/common/**",
"**/vs/platform/*/test/common/**"
]
Expand Down
File renamed without changes.
211 changes: 211 additions & 0 deletions src/vs/platform/undoRedo/test/common/undoRedoService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { UndoRedoElementType, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { mock } from 'vs/base/test/common/mock';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';

suite('UndoRedoService', () => {

function createUndoRedoService(dialogService: IDialogService = new TestDialogService()): UndoRedoService {
const notificationService = new TestNotificationService();
return new UndoRedoService(dialogService, notificationService);
}

test('simple single resource elements', () => {
const resource = URI.file('test.txt');
const service = createUndoRedoService();

assert.equal(service.canUndo(resource), false);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), false);
assert.ok(service.getLastElement(resource) === null);

let undoCall1 = 0;
let redoCall1 = 0;
const element1: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: resource,
label: 'typing 1',
undo: () => { undoCall1++; },
redo: () => { redoCall1++; }
};
service.pushElement(element1);

assert.equal(undoCall1, 0);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element1);

service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource), false);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);

service.redo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element1);

let undoCall2 = 0;
let redoCall2 = 0;
const element2: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: resource,
label: 'typing 2',
undo: () => { undoCall2++; },
redo: () => { redoCall2++; }
};
service.pushElement(element2);

assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 0);
assert.equal(redoCall2, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element2);

service.undo(resource);

assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);

let undoCall3 = 0;
let redoCall3 = 0;
const element3: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: resource,
label: 'typing 2',
undo: () => { undoCall3++; },
redo: () => { redoCall3++; }
};
service.pushElement(element3);

assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(undoCall3, 0);
assert.equal(redoCall3, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element3);

service.undo(resource);

assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(undoCall3, 1);
assert.equal(redoCall3, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
});

test('multi resource elements', async () => {
const resource1 = URI.file('test1.txt');
const resource2 = URI.file('test2.txt');
const service = createUndoRedoService(new class extends mock<IDialogService>() {
async show() {
return {
choice: 0 // confirm!
};
}
});

let undoCall1 = 0, undoCall11 = 0, undoCall12 = 0;
let redoCall1 = 0, redoCall11 = 0, redoCall12 = 0;
const element1: IUndoRedoElement = {
type: UndoRedoElementType.Workspace,
resources: [resource1, resource2],
label: 'typing 1',
undo: () => { undoCall1++; },
redo: () => { redoCall1++; },
split: () => {
return [
{
type: UndoRedoElementType.Resource,
resource: resource1,
label: 'typing 1.1',
undo: () => { undoCall11++; },
redo: () => { redoCall11++; }
},
{
type: UndoRedoElementType.Resource,
resource: resource2,
label: 'typing 1.2',
undo: () => { undoCall12++; },
redo: () => { redoCall12++; }
}
];
}
};
service.pushElement(element1);

assert.equal(service.canUndo(resource1), true);
assert.equal(service.canRedo(resource1), false);
assert.equal(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === element1);
assert.equal(service.canUndo(resource2), true);
assert.equal(service.canRedo(resource2), false);
assert.equal(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === element1);

await service.undo(resource1);

assert.equal(undoCall1, 1);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource1), false);
assert.equal(service.canRedo(resource1), true);
assert.equal(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === null);
assert.equal(service.canUndo(resource2), false);
assert.equal(service.canRedo(resource2), true);
assert.equal(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === null);

await service.redo(resource2);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall11, 0);
assert.equal(redoCall11, 0);
assert.equal(undoCall12, 0);
assert.equal(redoCall12, 0);
assert.equal(service.canUndo(resource1), true);
assert.equal(service.canRedo(resource1), false);
assert.equal(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === element1);
assert.equal(service.canUndo(resource2), true);
assert.equal(service.canRedo(resource2), false);
assert.equal(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === element1);

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (e) {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.RemotePreconditionFailed:
case UserDataSyncErrorCode.PreconditionFailed:
// Rejected as there is a new remote version. Syncing again...
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);

Expand Down
17 changes: 9 additions & 8 deletions src/vs/platform/userDataSync/common/userDataSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,19 @@ export interface IUserDataSyncBackupStoreService {
// #region User Data Sync Error

export enum UserDataSyncErrorCode {
// Server Errors
Unauthorized = 'Unauthorized',
Forbidden = 'Forbidden',
// Client Errors (>= 400 )
Unauthorized = 'Unauthorized', /* 401 */
PreconditionFailed = 'PreconditionFailed', /* 412 */
TooLarge = 'TooLarge', /* 413 */
UpgradeRequired = 'UpgradeRequired', /* 426 */
PreconditionRequired = 'PreconditionRequired', /* 428 */
TooManyRequests = 'RemoteTooManyRequests', /* 429 */

// Local Errors
ConnectionRefused = 'ConnectionRefused',
RemotePreconditionFailed = 'RemotePreconditionFailed',
TooManyRequests = 'RemoteTooManyRequests',
TooLarge = 'TooLarge',
NoRef = 'NoRef',
TurnedOff = 'TurnedOff',
SessionExpired = 'SessionExpired',

// Local Errors
LocalTooManyRequests = 'LocalTooManyRequests',
LocalPreconditionFailed = 'LocalPreconditionFailed',
LocalInvalidContent = 'LocalInvalidContent',
Expand Down
61 changes: 39 additions & 22 deletions src/vs/platform/userDataSync/common/userDataSyncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
Expand All @@ -26,7 +26,7 @@ import { platform, PlatformToString } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';

type SyncClassification = {
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};

const SESSION_ID_KEY = 'sync.sessionId';
Expand Down Expand Up @@ -96,26 +96,40 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ

async pull(): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
try {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
}
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
}
this.updateLastSyncTime();
}

async push(): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
try {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
}
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
}
this.updateLastSyncTime();
}

private recoveredSettings: boolean = false;
Expand Down Expand Up @@ -169,7 +183,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
try {
await synchroniser.sync(manifest);
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
this.handleSynchronizerError(e, synchroniser.resource);
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
}
}
Expand All @@ -192,6 +206,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
this.updateLastSyncTime();

} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
} finally {
this.updateStatus();
this._onSyncErrors.fire(this._syncErrors);
Expand Down Expand Up @@ -374,18 +393,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}

private handleSyncError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncStoreError) {
private handleSynchronizerError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.TooLarge:
this.telemetryService.publicLog2<{ source: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { source });
break;
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.LocalTooManyRequests:
this.telemetryService.publicLog2(`sync/error/${e.code}`);
break;
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.Incompatible:
throw e;
}
throw e;
}
this.logService.error(e);
this.logService.error(`${source}: ${toErrorMessage(e)}`);
Expand Down
10 changes: 5 additions & 5 deletions src/vs/platform/userDataSync/common/userDataSyncStoreService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,18 +262,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized);
}

if (context.res.statusCode === 403) {
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden);
}

if (context.res.statusCode === 412) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.RemotePreconditionFailed);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed);
}

if (context.res.statusCode === 413) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge);
}

if (context.res.statusCode === 426) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired);
}

if (context.res.statusCode === 429) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests);
}
Expand Down
Loading

0 comments on commit 76590a7

Please sign in to comment.