WIP test client
This commit is contained in:
parent
fde1fecbb6
commit
4872f6d3b3
7 changed files with 287 additions and 2 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -7,8 +7,7 @@ node_modules
|
||||||
# Rust build folder
|
# Rust build folder
|
||||||
backend/target
|
backend/target
|
||||||
|
|
||||||
frontend/obsidian-plugin/dist
|
frontend/*/dist
|
||||||
frontend/sync-client/dist
|
|
||||||
|
|
||||||
backend/db.sqlite3*
|
backend/db.sqlite3*
|
||||||
backend/config.yml
|
backend/config.yml
|
||||||
|
|
|
||||||
107
frontend/test-client/src/agent/mock-agent.ts
Normal file
107
frontend/test-client/src/agent/mock-agent.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { choose } from "../utils/choose";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { assert } from "../utils/assert";
|
||||||
|
import { SyncSettings } from "sync-client";
|
||||||
|
import { MockClient } from "./mock-client";
|
||||||
|
|
||||||
|
export class MockAgent extends MockClient {
|
||||||
|
private writtenContents: Array<string> = [];
|
||||||
|
private pendingActions: Array<Promise<unknown>> = [];
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
globalFiles: Record<string, Uint8Array>,
|
||||||
|
initialSettings: Partial<SyncSettings>,
|
||||||
|
private readonly name: string
|
||||||
|
) {
|
||||||
|
super(globalFiles, initialSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async act(): Promise<void> {
|
||||||
|
let options: Array<() => Promise<unknown>> = [
|
||||||
|
() =>
|
||||||
|
this.create(
|
||||||
|
this.getFileName(),
|
||||||
|
new TextEncoder().encode(this.getContent())
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
this.client.settings.setSetting(
|
||||||
|
"fetchChangesUpdateIntervalMs",
|
||||||
|
Math.random() * 1000
|
||||||
|
),
|
||||||
|
() => this.client.settings.setSetting("isSyncEnabled", false),
|
||||||
|
() => 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),
|
||||||
|
(old) => old + " " + this.getContent()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingActions.push(choose(options)());
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContent() {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
this.writtenContents.push(uuid);
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFileName() {
|
||||||
|
return `${this.name}-${uuidv4()}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async finish(): Promise<void> {
|
||||||
|
await Promise.all(this.pendingActions);
|
||||||
|
await this.client.settings.setSetting("isSyncEnabled", true);
|
||||||
|
await this.client.syncer.applyRemoteChangesLocally();
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertFileSystemIsConsistent(): void {
|
||||||
|
const files = Object.keys(this.globalFiles);
|
||||||
|
const localFiles = Object.keys(this.files);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
files.length === localFiles.length,
|
||||||
|
`File count mismatch: ${files.length} != ${localFiles.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
assert(
|
||||||
|
file in this.globalFiles,
|
||||||
|
`File ${file} missing in global files`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
new TextDecoder().decode(this.globalFiles[file]) ===
|
||||||
|
new TextDecoder().decode(this.files[file]),
|
||||||
|
`File ${file} content mismatch`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertAllContentIsPresentOnce(): void {
|
||||||
|
for (const content of this.writtenContents) {
|
||||||
|
const found = Object.values(this.files).filter((file) => {
|
||||||
|
return new TextDecoder().decode(file).includes(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
frontend/test-client/src/agent/mock-client.ts
Normal file
125
frontend/test-client/src/agent/mock-client.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import {
|
||||||
|
SyncClient,
|
||||||
|
RelativePath,
|
||||||
|
FileSystemOperations,
|
||||||
|
SyncSettings
|
||||||
|
} from "sync-client";
|
||||||
|
import { assert } from "../utils/assert";
|
||||||
|
|
||||||
|
export class MockClient implements FileSystemOperations {
|
||||||
|
protected readonly files: Record<string, Uint8Array> = {};
|
||||||
|
protected client!: SyncClient;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected readonly globalFiles: Record<string, Uint8Array>,
|
||||||
|
private readonly initialSettings: Partial<SyncSettings>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async init() {
|
||||||
|
let _data: unknown = "";
|
||||||
|
|
||||||
|
this.client = await SyncClient.create(this, {
|
||||||
|
load: async () => _data,
|
||||||
|
save: async (data: unknown) => void (_data = data)
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(this.initialSettings).forEach((key) => {
|
||||||
|
this.client.settings.setSetting(
|
||||||
|
key as keyof SyncSettings,
|
||||||
|
this.initialSettings[key as keyof SyncSettings]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(
|
||||||
|
(await this.client.checkConnection()).isSuccessful,
|
||||||
|
"Connection check failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listAllFiles(): Promise<RelativePath[]> {
|
||||||
|
return Object.keys(this.files) as RelativePath[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async read(path: RelativePath): Promise<Uint8Array> {
|
||||||
|
return this.files[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFileSize(path: RelativePath): Promise<number> {
|
||||||
|
return this.files[path].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getModificationTime(path: RelativePath): Promise<Date> {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async exists(path: RelativePath): Promise<boolean> {
|
||||||
|
return path in this.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(
|
||||||
|
path: RelativePath,
|
||||||
|
newContent: Uint8Array
|
||||||
|
): Promise<void> {
|
||||||
|
this.globalFiles[path] = newContent;
|
||||||
|
this.files[path] = newContent;
|
||||||
|
this.client.syncer.syncLocallyCreatedFile(path, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createDirectory(path: RelativePath): Promise<void> {}
|
||||||
|
|
||||||
|
public async atomicUpdateText(
|
||||||
|
path: RelativePath,
|
||||||
|
updater: (currentContent: string) => string
|
||||||
|
): Promise<string> {
|
||||||
|
const currentContent = new TextDecoder().decode(this.files[path]);
|
||||||
|
const newContent = updater(currentContent);
|
||||||
|
const newContentUint8Array = new TextEncoder().encode(newContent);
|
||||||
|
this.globalFiles[path] = newContentUint8Array;
|
||||||
|
this.files[path] = newContentUint8Array;
|
||||||
|
this.client.syncer.syncLocallyUpdatedFile({
|
||||||
|
relativePath: path,
|
||||||
|
updateTime: new Date()
|
||||||
|
});
|
||||||
|
return newContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async write(path: RelativePath, content: Uint8Array): Promise<void> {
|
||||||
|
this.globalFiles[path] = content;
|
||||||
|
this.files[path] = content;
|
||||||
|
this.client.syncer.syncLocallyUpdatedFile({
|
||||||
|
relativePath: path,
|
||||||
|
updateTime: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(path: RelativePath): Promise<void> {
|
||||||
|
delete this.files[path];
|
||||||
|
if (path in this.globalFiles) {
|
||||||
|
delete this.globalFiles[path];
|
||||||
|
}
|
||||||
|
this.client.syncer.syncLocallyDeletedFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rename(
|
||||||
|
oldPath: RelativePath,
|
||||||
|
newPath: RelativePath
|
||||||
|
): Promise<void> {
|
||||||
|
this.files[newPath] = this.files[oldPath];
|
||||||
|
delete this.files[oldPath];
|
||||||
|
|
||||||
|
if (oldPath in this.globalFiles) {
|
||||||
|
this.globalFiles[newPath] = this.files[oldPath];
|
||||||
|
delete this.globalFiles[oldPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.syncer.syncLocallyUpdatedFile({
|
||||||
|
oldPath,
|
||||||
|
relativePath: newPath,
|
||||||
|
updateTime: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isFileEligibleForSync(path: RelativePath): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/test-client/src/cli.ts
Normal file
43
frontend/test-client/src/cli.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { SyncSettings } from "sync-client";
|
||||||
|
import { MockAgent } from "./agent/mock-agent";
|
||||||
|
import { sleep } from "./utils/sleep";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const globalFiles: Record<string, Uint8Array> = {};
|
||||||
|
const iterations = 100;
|
||||||
|
|
||||||
|
async function runTest(): Promise<void> {
|
||||||
|
console.info("Starting test...");
|
||||||
|
|
||||||
|
const initialSettings: Partial<SyncSettings> = {
|
||||||
|
isSyncEnabled: true,
|
||||||
|
token: "token",
|
||||||
|
vaultName: uuidv4()
|
||||||
|
};
|
||||||
|
|
||||||
|
const clients = [
|
||||||
|
new MockAgent(globalFiles, initialSettings, "agent-1"),
|
||||||
|
new MockAgent(globalFiles, initialSettings, "agent-2"),
|
||||||
|
new MockAgent(globalFiles, initialSettings, "agent-3"),
|
||||||
|
new MockAgent(globalFiles, initialSettings, "agent-4"),
|
||||||
|
new MockAgent(globalFiles, initialSettings, "agent-5")
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(clients.map((client) => client.init()));
|
||||||
|
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
await Promise.all(clients.map((client) => client.act()));
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(clients.map((client) => client.finish()));
|
||||||
|
|
||||||
|
clients.forEach((client) => {
|
||||||
|
client.assertFileSystemIsConsistent();
|
||||||
|
client.assertAllContentIsPresentOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info("Test completed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
5
frontend/test-client/src/utils/assert.ts
Normal file
5
frontend/test-client/src/utils/assert.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function assert(value: boolean, message: string): asserts value {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
frontend/test-client/src/utils/choose.ts
Normal file
3
frontend/test-client/src/utils/choose.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function choose<T>(values: T[]): T {
|
||||||
|
return values[Math.floor(Math.random() * values.length)];
|
||||||
|
}
|
||||||
3
frontend/test-client/src/utils/sleep.ts
Normal file
3
frontend/test-client/src/utils/sleep.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue