mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 01:25:38 -05:00
Merge from vscode 5d18ad4c5902e3bddbc9f78da82dfc2ac349e908 (#9683)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
@@ -110,6 +110,9 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
async sync(ref?: string): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
await this.stop();
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
@@ -264,6 +267,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
|
||||
abstract stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreviewResult {
|
||||
@@ -299,7 +303,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.cancel();
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
try {
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
@@ -339,7 +343,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed);
|
||||
} else {
|
||||
|
||||
202
src/vs/platform/userDataSync/common/snippetsMerge.ts
Normal file
202
src/vs/platform/userDataSync/common/snippetsMerge.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
|
||||
export interface IMergeResult {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
conflicts: string[];
|
||||
remote: IStringDictionary<string> | null;
|
||||
}
|
||||
|
||||
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null, resolvedConflicts: IStringDictionary<string | null> = {}): IMergeResult {
|
||||
const added: IStringDictionary<string> = {};
|
||||
const updated: IStringDictionary<string> = {};
|
||||
const removed: Set<string> = new Set<string>();
|
||||
|
||||
if (!remote) {
|
||||
return {
|
||||
added,
|
||||
removed: values(removed),
|
||||
updated,
|
||||
conflicts: [],
|
||||
remote: local
|
||||
};
|
||||
}
|
||||
|
||||
const localToRemote = compare(local, remote);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return {
|
||||
added,
|
||||
removed: values(removed),
|
||||
updated,
|
||||
conflicts: [],
|
||||
remote: null
|
||||
};
|
||||
}
|
||||
|
||||
const baseToLocal = compare(base, local);
|
||||
const baseToRemote = compare(base, remote);
|
||||
const remoteContent: IStringDictionary<string> = deepClone(remote);
|
||||
const conflicts: Set<string> = new Set<string>();
|
||||
const handledConflicts: Set<string> = new Set<string>();
|
||||
const handleConflict = (key: string): void => {
|
||||
if (handledConflicts.has(key)) {
|
||||
return;
|
||||
}
|
||||
handledConflicts.add(key);
|
||||
const conflictContent = resolvedConflicts[key];
|
||||
|
||||
// add to conflicts
|
||||
if (conflictContent === undefined) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
|
||||
// remove the snippet
|
||||
else if (conflictContent === null) {
|
||||
delete remote[key];
|
||||
if (local[key]) {
|
||||
removed.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// add/update the snippet
|
||||
else {
|
||||
if (local[key]) {
|
||||
if (local[key] !== conflictContent) {
|
||||
updated[key] = conflictContent;
|
||||
}
|
||||
} else {
|
||||
added[key] = conflictContent;
|
||||
}
|
||||
remoteContent[key] = conflictContent;
|
||||
}
|
||||
};
|
||||
|
||||
// Removed snippets in Local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Add to local
|
||||
added[key] = remote[key];
|
||||
}
|
||||
// Remove it in remote
|
||||
else {
|
||||
delete remoteContent[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Removed snippets in Remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Conflict - Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
// Also remove in Local
|
||||
else {
|
||||
removed.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else if (local[key] !== undefined) {
|
||||
updated[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
added[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent };
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = from ? Object.keys(from) : [];
|
||||
const toKeys = to ? Object.keys(to) : [];
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const fromSnippet = from![key]!;
|
||||
const toSnippet = to![key]!;
|
||||
if (fromSnippet !== toSnippet) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
|
||||
const { added, removed, updated } = compare(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
403
src/vs/platform/userDataSync/common/snippetsSync.ts
Normal file
403
src/vs/platform/userDataSync/common/snippetsSync.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename } from 'vs/base/common/resources';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly local: IStringDictionary<IFileContent>;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly added: IStringDictionary<string>;
|
||||
readonly updated: IStringDictionary<string>;
|
||||
readonly removed: string[];
|
||||
readonly conflicts: Conflict[];
|
||||
readonly resolvedConflicts: IStringDictionary<string | null>;
|
||||
readonly remote: IStringDictionary<string> | null;
|
||||
}
|
||||
|
||||
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
private readonly snippetsFolder: URI;
|
||||
private readonly snippetsPreviewFolder: URI;
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(SyncResource.Snippets, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
|
||||
this.snippetsFolder = environmentService.snippetsHome;
|
||||
this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
|
||||
this._register(this.fileService.watch(environmentService.userRoamingDataHome));
|
||||
this._register(this.fileService.watch(this.snippetsFolder));
|
||||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
if (!e.changes.some(change => isEqualOrParent(change.resource, this.snippetsFolder))) {
|
||||
return;
|
||||
}
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Sync again if local file has changed and current status is in conflicts
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.syncPreviewResultPromise!.then(result => {
|
||||
this.cancel();
|
||||
this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status));
|
||||
});
|
||||
}
|
||||
// Otherwise fire change event
|
||||
else {
|
||||
this._onDidChangeLocal.fire();
|
||||
}
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling snippets as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
try {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Started pulling snippets...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}
|
||||
}));
|
||||
await this.apply();
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
else {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Remote snippets does not exist.`);
|
||||
}
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling snippets.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing snippets as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
try {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Started pushing snippets...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const { added, removed, updated, remote } = merge(localSnippets, null, null);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}
|
||||
}));
|
||||
|
||||
await this.apply(true);
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing snippets.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.clearConflicts();
|
||||
this.cancel();
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.syncResourceLogLabel}.`);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
if (isEqualOrParent(conflictResource.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder) && this.syncPreviewResultPromise) {
|
||||
const result = await this.syncPreviewResultPromise;
|
||||
const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!;
|
||||
if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) {
|
||||
return result.local[key] ? result.local[key].value.toString() : null;
|
||||
} else if (result.remoteUserData && result.remoteUserData.syncData) {
|
||||
const snippets = this.parseSnippets(result.remoteUserData.syncData);
|
||||
return snippets[key] || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getRemoteContent(ref?: string, fragment?: string): Promise<string | null> {
|
||||
const content = await super.getRemoteContent(ref);
|
||||
if (content !== null && fragment) {
|
||||
return this.getFragment(content, fragment);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> {
|
||||
let content = await super.getLocalBackupContent(ref);
|
||||
if (content !== null && fragment) {
|
||||
return this.getFragment(content, fragment);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private getFragment(content: string, fragment: string): string | null {
|
||||
const syncData = this.parseSyncData(content);
|
||||
return syncData ? this.getFragmentFromSyncData(syncData, fragment) : null;
|
||||
}
|
||||
|
||||
private getFragmentFromSyncData(syncData: ISyncData, fragment: string): string | null {
|
||||
switch (fragment) {
|
||||
case 'snippets':
|
||||
return syncData.content;
|
||||
default:
|
||||
const remoteSnippets = this.parseSnippets(syncData);
|
||||
return remoteSnippets[fragment] || null;
|
||||
}
|
||||
}
|
||||
|
||||
async acceptConflict(conflictResource: URI, content: string): Promise<void> {
|
||||
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
|
||||
if (this.status === SyncStatus.HasConflicts && conflict) {
|
||||
const key = relativePath(this.snippetsPreviewFolder, conflict.local)!;
|
||||
let previewResult = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
previewResult.resolvedConflicts[key] = content || null;
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token));
|
||||
previewResult = await this.syncPreviewResultPromise;
|
||||
this.setConflicts(previewResult.conflicts);
|
||||
if (!this.conflicts.length) {
|
||||
await this.apply();
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localSnippets = await this.getSnippetsFileContents();
|
||||
if (Object.keys(localSnippets).length) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
/* ignore error */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
try {
|
||||
const previewResult = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
this.setConflicts(previewResult.conflicts);
|
||||
if (this.conflicts.length) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
await this.apply();
|
||||
return SyncStatus.Idle;
|
||||
} catch (e) {
|
||||
this.syncPreviewResultPromise = null;
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
case UserDataSyncErrorCode.LocalPreconditionFailed:
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize snippets as there is a new local version available. Synchronizing again...`);
|
||||
return this.performSync(remoteUserData, lastSyncUserData);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents()
|
||||
.then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token)));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
protected cancel(): void {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async clearConflicts(): Promise<void> {
|
||||
if (this.conflicts.length) {
|
||||
await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local)));
|
||||
this.setConflicts([]);
|
||||
}
|
||||
}
|
||||
|
||||
private async generatePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null>, token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null;
|
||||
|
||||
if (remoteSnippets) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
|
||||
}
|
||||
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
|
||||
|
||||
const conflicts: Conflict[] = [];
|
||||
for (const key of mergeResult.conflicts) {
|
||||
const localPreview = joinPath(this.snippetsPreviewFolder, key);
|
||||
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
|
||||
const content = local[key];
|
||||
if (!token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString(''));
|
||||
}
|
||||
}
|
||||
|
||||
for (const conflict of this.conflicts) {
|
||||
// clear obsolete conflicts
|
||||
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
|
||||
try {
|
||||
await this.fileService.del(conflict.local);
|
||||
} catch (error) {
|
||||
// Ignore & log
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote, resolvedConflicts };
|
||||
}
|
||||
|
||||
private async apply(forcePush?: boolean): Promise<void> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData } = await this.syncPreviewResultPromise;
|
||||
|
||||
const hasChanges = Object.keys(added).length || removed.length || Object.keys(updated).length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (Object.keys(added).length || removed.length || Object.keys(updated).length) {
|
||||
// back up all snippets
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
await this.updateLocalSnippets(added, removed, updated, local);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
|
||||
const content = JSON.stringify(remote);
|
||||
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
// update last sync
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized snippets...`);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
|
||||
}
|
||||
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(added: IStringDictionary<string>, removed: string[], updated: IStringDictionary<string>, local: IStringDictionary<IFileContent>): Promise<void> {
|
||||
for (const key of removed) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
|
||||
await this.fileService.del(resource);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
|
||||
}
|
||||
|
||||
for (const key of Object.keys(added)) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
|
||||
}
|
||||
|
||||
for (const key of Object.keys(updated)) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
|
||||
}
|
||||
}
|
||||
|
||||
private parseSnippets(syncData: ISyncData): IStringDictionary<string> {
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
private toSnippetsContents(snippetsFileContents: IStringDictionary<IFileContent>): IStringDictionary<string> {
|
||||
const snippets: IStringDictionary<string> = {};
|
||||
for (const key of Object.keys(snippetsFileContents)) {
|
||||
snippets[key] = snippetsFileContents[key].value.toString();
|
||||
}
|
||||
return snippets;
|
||||
}
|
||||
|
||||
private async getSnippetsFileContents(): Promise<IStringDictionary<IFileContent>> {
|
||||
const snippets: IStringDictionary<IFileContent> = {};
|
||||
let stat: IFileStat;
|
||||
try {
|
||||
stat = await this.fileService.resolve(this.snippetsFolder);
|
||||
} catch (e) {
|
||||
// No snippets
|
||||
if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
|
||||
return snippets;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
for (const entry of stat.children || []) {
|
||||
const resource = entry.resource;
|
||||
if (extname(resource) === '.json') {
|
||||
const key = relativePath(this.snippetsFolder, resource)!;
|
||||
const content = await this.fileService.readFile(resource);
|
||||
snippets[key] = content;
|
||||
}
|
||||
}
|
||||
return snippets;
|
||||
}
|
||||
}
|
||||
@@ -138,10 +138,11 @@ export function getUserDataSyncStore(productService: IProductService, configurat
|
||||
export const enum SyncResource {
|
||||
Settings = 'settings',
|
||||
Keybindings = 'keybindings',
|
||||
Snippets = 'snippets',
|
||||
Extensions = 'extensions',
|
||||
GlobalState = 'globalState'
|
||||
}
|
||||
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Extensions, SyncResource.GlobalState];
|
||||
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState];
|
||||
|
||||
export interface IUserDataManifest {
|
||||
latest?: Record<SyncResource, string>
|
||||
@@ -373,10 +374,3 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe
|
||||
localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme });
|
||||
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0];
|
||||
}
|
||||
export function getSyncResourceFromRemotePreview(remotePreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
|
||||
if (remotePreview.scheme !== USER_DATA_SYNC_SCHEME) {
|
||||
return undefined;
|
||||
}
|
||||
remotePreview = remotePreview.with({ scheme: environmentService.userDataSyncHome.scheme });
|
||||
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(remotePreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -55,6 +56,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
private readonly settingsSynchroniser: SettingsSynchroniser;
|
||||
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
|
||||
private readonly snippetsSynchroniser: SnippetsSynchroniser;
|
||||
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
|
||||
private readonly globalStateSynchroniser: GlobalStateSynchroniser;
|
||||
|
||||
@@ -68,9 +70,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
super();
|
||||
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
|
||||
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
|
||||
this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser));
|
||||
this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser));
|
||||
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
|
||||
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
|
||||
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
|
||||
this.updateStatus();
|
||||
|
||||
if (this.userDataSyncStoreService.userDataSyncStore) {
|
||||
|
||||
436
src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts
Normal file
436
src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
|
||||
const tsSnippet1 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const tsSnippet2 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console always",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const htmlSnippet1 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div"
|
||||
}
|
||||
}`;
|
||||
|
||||
const htmlSnippet2 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div changed"
|
||||
}
|
||||
}`;
|
||||
|
||||
const cSnippet = `{
|
||||
// Place your snippets for c here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position.Placeholders with the
|
||||
// same ids are connected.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
}`;
|
||||
|
||||
suite('SnippetsMerge', () => {
|
||||
|
||||
test('merge when local and remote are same with one snippet', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
const local = { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const base = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
const local = {};
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, remote);
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when new entry is added to remote from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when all entries are removed from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = {};
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entries are added to local', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
const base = { 'html.json': htmlSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2 };
|
||||
const remote = { 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge with single entry and local is empty', async () => {
|
||||
const base = { 'html.json': htmlSnippet1 };
|
||||
const local = {};
|
||||
const remote = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
const resolvedConflicts = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
const resolvedConflicts = { 'html.json': null };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['html.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'c.json': cSnippet };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'c.json': cSnippet };
|
||||
const resolvedConflicts = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 });
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['typescript.json']);
|
||||
assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 });
|
||||
});
|
||||
|
||||
});
|
||||
614
src/vs/platform/userDataSync/test/common/snippetsSync.test.ts
Normal file
614
src/vs/platform/userDataSync/test/common/snippetsSync.test.ts
Normal file
@@ -0,0 +1,614 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
|
||||
const tsSnippet1 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const tsSnippet2 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console always",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const htmlSnippet1 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div"
|
||||
}
|
||||
}`;
|
||||
|
||||
const htmlSnippet2 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div changed"
|
||||
}
|
||||
}`;
|
||||
|
||||
const htmlSnippet3 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div changed again"
|
||||
}
|
||||
}`;
|
||||
|
||||
suite('SnippetsSync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let testClient: UserDataSyncClient;
|
||||
let client2: UserDataSyncClient;
|
||||
|
||||
let testObject: SnippetsSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
testClient = disposableStore.add(new UserDataSyncClient(server));
|
||||
await testClient.setUp(true);
|
||||
testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Snippets) as SnippetsSynchroniser;
|
||||
disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
|
||||
client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('first time sync - outgoing to server (no snippets)', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync - incoming from server (no snippets)', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has conflicts and accept conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
const conflicts = testObject.conflicts;
|
||||
await testObject.acceptConflict(conflicts[0].local, htmlSnippet1);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(conflicts[0].local));
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
|
||||
assertConflicts(testObject.conflicts, [
|
||||
{ local: local1, remote: local1.with({ scheme: USER_DATA_SYNC_SCHEME }) },
|
||||
{ local: local2, remote: local2.with({ scheme: USER_DATA_SYNC_SCHEME }) }
|
||||
]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
let conflicts = testObject.conflicts;
|
||||
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(conflicts[0].local));
|
||||
|
||||
conflicts = testObject.conflicts;
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
const conflicts = testObject.conflicts;
|
||||
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
|
||||
await testObject.acceptConflict(conflicts[1].local, tsSnippet1);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(conflicts[0].local));
|
||||
assert.ok(!await fileService.exists(conflicts[1].local));
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync adding a snippet', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync adding a snippet - accept', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
});
|
||||
|
||||
test('sync updating a snippet', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet2 });
|
||||
});
|
||||
|
||||
test('sync updating a snippet - accept', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
});
|
||||
|
||||
test('sync updating a snippet - conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet3, testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
});
|
||||
|
||||
test('sync updating a snippet - resolve conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet3, testClient);
|
||||
await testObject.sync();
|
||||
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet2 });
|
||||
});
|
||||
|
||||
test('sync removing a snippet', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
await removeSnippet('html.json', testClient);
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, null);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync removing a snippet - accept', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, null);
|
||||
});
|
||||
|
||||
test('sync removing a snippet locally and updating it remotely', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await removeSnippet('html.json', testClient);
|
||||
await testObject.sync();
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, htmlSnippet2);
|
||||
});
|
||||
|
||||
test('sync removing a snippet - conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
});
|
||||
|
||||
test('sync removing a snippet - resolve conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, htmlSnippet3);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet3 });
|
||||
});
|
||||
|
||||
test('sync removing a snippet - resolve conflict by removing', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync();
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync();
|
||||
await testObject.acceptConflict(testObject.conflicts[0].local, '');
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, null);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync - push', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
|
||||
await testObject.push();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync - pull', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.pull();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
});
|
||||
|
||||
function parseSnippets(content: string): IStringDictionary<string> {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
async function updateSnippet(name: string, content: string, client: UserDataSyncClient): Promise<void> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const snippetsResource = joinPath(environmentService.snippetsHome, name);
|
||||
await fileService.writeFile(snippetsResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
async function removeSnippet(name: string, client: UserDataSyncClient): Promise<void> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const snippetsResource = joinPath(environmentService.snippetsHome, name);
|
||||
await fileService.del(snippetsResource);
|
||||
}
|
||||
|
||||
async function readSnippet(name: string, client: UserDataSyncClient): Promise<string | null> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const snippetsResource = joinPath(environmentService.snippetsHome, name);
|
||||
if (await fileService.exists(snippetsResource)) {
|
||||
const content = await fileService.readFile(snippetsResource);
|
||||
return content.value.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function assertConflicts(actual: Conflict[], expected: Conflict[]) {
|
||||
assert.deepEqual(actual.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })), expected.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -44,7 +44,7 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } });
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
async stop(): Promise<void> {
|
||||
this.cancelled = true;
|
||||
this.syncBarrier.open();
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export class UserDataSyncClient extends Disposable {
|
||||
userDataSyncHome,
|
||||
settingsResource: joinPath(userDataDirectory, 'settings.json'),
|
||||
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
|
||||
snippetsHome: joinPath(userDataDirectory, 'snippets'),
|
||||
argvResource: joinPath(userDataDirectory, 'argv.json'),
|
||||
args: {}
|
||||
});
|
||||
@@ -108,6 +109,7 @@ export class UserDataSyncClient extends Disposable {
|
||||
if (!empty) {
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({})));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'c.json'), VSBuffer.fromString(`{}`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
|
||||
}
|
||||
await configurationService.reloadConfiguration();
|
||||
@@ -201,16 +203,13 @@ export class UserDataSyncTestServer implements IRequestService {
|
||||
}
|
||||
|
||||
private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise<IRequestContext> {
|
||||
if (!headers['If-Match']) {
|
||||
return this.toResponse(428);
|
||||
}
|
||||
if (!this.session) {
|
||||
this.session = generateUuid();
|
||||
}
|
||||
const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource);
|
||||
if (resourceKey) {
|
||||
const data = this.data.get(resourceKey);
|
||||
if (headers['If-Match'] !== (data ? data.ref : '0')) {
|
||||
if (headers['If-Match'] !== undefined && headers['If-Match'] !== (data ? data.ref : '0')) {
|
||||
return this.toResponse(412);
|
||||
}
|
||||
const ref = `${parseInt(data?.ref || '0') + 1}`;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
|
||||
suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing tests
|
||||
|
||||
@@ -36,6 +37,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
|
||||
@@ -65,6 +69,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
|
||||
@@ -102,6 +109,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
@@ -140,6 +149,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
@@ -174,6 +185,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
@@ -198,6 +211,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (merge) from the test client
|
||||
@@ -215,6 +229,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
@@ -258,6 +275,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
|
||||
// Sync from the client
|
||||
@@ -270,6 +288,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
]);
|
||||
@@ -294,6 +314,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{ "a": "changed" }`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
@@ -308,6 +329,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
@@ -359,6 +382,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
|
||||
@@ -454,7 +480,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.sync();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
|
||||
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
|
||||
});
|
||||
|
||||
test('test sync conflicts status', async () => {
|
||||
|
||||
Reference in New Issue
Block a user