diff --git a/frontend/test-client/src/agent/mock-agent.ts b/frontend/test-client/src/agent/mock-agent.ts index 2e320819..c69fae45 100644 --- a/frontend/test-client/src/agent/mock-agent.ts +++ b/frontend/test-client/src/agent/mock-agent.ts @@ -4,6 +4,7 @@ import { assert } from "../utils/assert"; import { LogLevel, SyncSettings } from "sync-client"; import { MockClient } from "./mock-client"; import chalk from "chalk"; +import { sleep } from "../utils/sleep"; export class MockAgent extends MockClient { private writtenContents: Array = []; @@ -13,7 +14,8 @@ export class MockAgent extends MockClient { globalFiles: Record, initialSettings: Partial, public readonly name: string, - private readonly color: string + private readonly color: string, + private readonly doDeletes: boolean ) { super(globalFiles, initialSettings); } @@ -47,31 +49,60 @@ export class MockAgent extends MockClient { public async act(): Promise { let options: Array<() => Promise> = [ - () => - this.create( - this.getFileName(), + () => { + const file = this.getFileName(); + this.client.logger.info(`Decided to create file ${file}`); + return this.create( + file, new TextEncoder().encode(this.getContent()) - ), - () => - this.client.settings.setSetting( + ); + }, + () => { + this.client.logger.info( + `Decided to change fetchChangesUpdateIntervalMs` + ); + return this.client.settings.setSetting( "fetchChangesUpdateIntervalMs", Math.random() * 1000 - ), - () => this.client.settings.setSetting("isSyncEnabled", false), - () => this.client.settings.setSetting("isSyncEnabled", true) + ); + }, + () => { + this.client.logger.info(`Decided to disable sync`); + return this.client.settings.setSetting("isSyncEnabled", false); + }, + () => { + this.client.logger.info(`Decided to enable sync`); + return this.client.settings.setSetting("isSyncEnabled", true); + } ]; let files = await this.listAllFiles(); if (files.length > 0) { options.push( - () => this.rename(choose(files), this.getFileName()), - () => - this.atomicUpdateText( - choose(files), + () => { + const file = choose(files); + + const newName = this.getFileName(); + this.client.logger.info( + `Decided to rename file ${file} to ${newName}` + ); + return this.rename(file, newName); + }, + () => { + const file = choose(files); + + this.client.logger.info(`Decided to update file ${file}`); + return this.atomicUpdateText( + file, (old) => old + " " + this.getContent() - ) + ); + } ); + + if (this.doDeletes) { + options.push(() => this.delete(choose(files))); + } } this.pendingActions.push(choose(options)()); @@ -91,47 +122,68 @@ export class MockAgent extends MockClient { await Promise.all(this.pendingActions); await this.client.settings.setSetting("isSyncEnabled", true); await this.client.syncer.applyRemoteChangesLocally(); + await sleep(5000); + await this.client.syncer.waitForSyncQueue(); this.client.stop(); } public assertFileSystemIsConsistent(): void { - const files = Object.keys(this.globalFiles); - const localFiles = Object.keys(this.files); + const globalFiles = Object.keys(this.globalFiles); + const localFiles = Object.keys(this.localFiles); - assert( - files.length === localFiles.length, - `File count mismatch: ${files.length} != ${localFiles.length}` + const missingInGlobal = localFiles.filter( + (file) => !(file in this.globalFiles) + ); + const missingInLocal = globalFiles.filter( + (file) => !(file in this.localFiles) ); - for (const file of files) { - assert( - file in this.globalFiles, - `File ${file} missing in global files` + assert( + missingInGlobal.length === 0, + `Files missing in global files: ${missingInGlobal.join(", ")}` + ); + assert( + missingInLocal.length === 0, + `Files missing in local files: ${missingInLocal.join(", ")}` + ); + + for (const file of globalFiles) { + const localContent = new TextDecoder().decode( + this.localFiles[file] + ); + const globalContent = new TextDecoder().decode( + this.globalFiles[file] ); assert( - new TextDecoder().decode(this.globalFiles[file]) === - new TextDecoder().decode(this.files[file]), - `File ${file} content mismatch` + localContent === globalContent, + `Content mismatch for file ${file}: ${localContent} <> ${globalContent}` ); } } public assertAllContentIsPresentOnce(): void { for (const content of this.writtenContents) { - const found = Object.values(this.files).filter((file) => { + const found = Object.values(this.localFiles).filter((file) => { return new TextDecoder().decode(file).includes(content); }); - assert( - found.length === 1, - `Content ${content} found in ${found.length} files` - ); + if (this.doDeletes) { + assert( + found.length <= 1, + `Content ${content} found in ${found.length} files` + ); + } else { + assert( + found.length === 1, + `Content ${content} found in ${found.length} files` + ); - const file = found[0]; - assert( - new TextDecoder().decode(file).split(content).length === 2, - `Content ${content} found more than once in a file` - ); + const file = found[0]; + assert( + new TextDecoder().decode(file).split(content).length === 2, + `Content ${content} found more than once in a file` + ); + } } } } diff --git a/frontend/test-client/src/agent/mock-client.ts b/frontend/test-client/src/agent/mock-client.ts index a967f54f..5766bc02 100644 --- a/frontend/test-client/src/agent/mock-client.ts +++ b/frontend/test-client/src/agent/mock-client.ts @@ -7,7 +7,7 @@ import { import { assert } from "../utils/assert"; export class MockClient implements FileSystemOperations { - protected readonly files: Record = {}; + protected readonly localFiles: Record = {}; protected client!: SyncClient; public constructor( @@ -37,31 +37,43 @@ export class MockClient implements FileSystemOperations { } public async listAllFiles(): Promise { - return Object.keys(this.files) as RelativePath[]; + return Object.keys(this.localFiles) as RelativePath[]; } public async read(path: RelativePath): Promise { - return this.files[path]; + if (!(path in this.localFiles)) { + throw new Error(`File ${path} does not exist`); + } + return this.localFiles[path]; } public async getFileSize(path: RelativePath): Promise { - return this.files[path].length; + if (!(path in this.localFiles)) { + throw new Error(`File ${path} does not exist`); + } + return this.localFiles[path].length; } public async getModificationTime(path: RelativePath): Promise { + if (!(path in this.localFiles)) { + throw new Error(`File ${path} does not exist`); + } return new Date(); } public async exists(path: RelativePath): Promise { - return path in this.files; + return path in this.localFiles; } public async create( path: RelativePath, newContent: Uint8Array ): Promise { + if (path in this.localFiles) { + throw new Error(`File ${path} already exists`); + } this.globalFiles[path] = newContent; - this.files[path] = newContent; + this.localFiles[path] = newContent; this.client.syncer.syncLocallyCreatedFile(path, new Date()); } @@ -71,55 +83,62 @@ export class MockClient implements FileSystemOperations { path: RelativePath, updater: (currentContent: string) => string ): Promise { - const currentContent = new TextDecoder().decode(this.files[path]); + if (!(path in this.localFiles)) { + throw new Error(`File ${path} does not exist`); + } + const currentContent = new TextDecoder().decode(this.localFiles[path]); const newContent = updater(currentContent); const newContentUint8Array = new TextEncoder().encode(newContent); this.globalFiles[path] = newContentUint8Array; - this.files[path] = newContentUint8Array; - this.client.syncer.syncLocallyUpdatedFile({ + this.localFiles[path] = newContentUint8Array; + + void this.client.syncer.syncLocallyUpdatedFile({ relativePath: path, updateTime: new Date() }); + return newContent; } public async write(path: RelativePath, content: Uint8Array): Promise { this.globalFiles[path] = content; - this.files[path] = content; - this.client.syncer.syncLocallyUpdatedFile({ + this.localFiles[path] = content; + + void this.client.syncer.syncLocallyUpdatedFile({ relativePath: path, updateTime: new Date() }); } public async delete(path: RelativePath): Promise { - delete this.files[path]; + delete this.localFiles[path]; if (path in this.globalFiles) { delete this.globalFiles[path]; } - this.client.syncer.syncLocallyDeletedFile(path); + + void this.client.syncer.syncLocallyDeletedFile(path); } public async rename( oldPath: RelativePath, newPath: RelativePath ): Promise { - this.files[newPath] = this.files[oldPath]; - delete this.files[oldPath]; + if (!(oldPath in this.localFiles)) { + throw new Error(`File ${oldPath} does not exist`); + } + + this.localFiles[newPath] = this.localFiles[oldPath]; + delete this.localFiles[oldPath]; if (oldPath in this.globalFiles) { - this.globalFiles[newPath] = this.files[oldPath]; + this.globalFiles[newPath] = this.localFiles[oldPath]; delete this.globalFiles[oldPath]; } - this.client.syncer.syncLocallyUpdatedFile({ + void this.client.syncer.syncLocallyUpdatedFile({ oldPath, relativePath: newPath, updateTime: new Date() }); } - - public isFileEligibleForSync(path: RelativePath): boolean { - return true; - } } diff --git a/frontend/test-client/src/cli.ts b/frontend/test-client/src/cli.ts index 7bb952cb..0d59f796 100644 --- a/frontend/test-client/src/cli.ts +++ b/frontend/test-client/src/cli.ts @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from "uuid"; const globalFiles: Record = {}; const iterations = 100; +const doDeletes = false; async function runTest(): Promise { console.info("Starting test"); @@ -17,11 +18,41 @@ async function runTest(): Promise { }; const clients = [ - new MockAgent(globalFiles, initialSettings, "agent-1", "#ff0000"), - new MockAgent(globalFiles, initialSettings, "agent-2", "#00ff00"), - new MockAgent(globalFiles, initialSettings, "agent-3", "#0000ff"), - new MockAgent(globalFiles, initialSettings, "agent-4", "#ffaa00"), - new MockAgent(globalFiles, initialSettings, "agent-5", "#00ffaa") + new MockAgent( + globalFiles, + initialSettings, + "agent-1", + "#ff0000", + doDeletes + ), + new MockAgent( + globalFiles, + initialSettings, + "agent-2", + "#00ff00", + doDeletes + ), + new MockAgent( + globalFiles, + initialSettings, + "agent-3", + "#0000ff", + doDeletes + ), + new MockAgent( + globalFiles, + initialSettings, + "agent-4", + "#ffaa00", + doDeletes + ), + new MockAgent( + globalFiles, + initialSettings, + "agent-5", + "#00ffaa", + doDeletes + ) ]; await Promise.all(clients.map((client) => client.init()));