Refactor tests

This commit is contained in:
Andras Schmelczer 2026-01-18 13:46:59 +00:00
parent 16afe31e89
commit f53ac121e8
19 changed files with 352 additions and 570 deletions

View file

@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { choose } from "../utils/choose";
import { v4 as uuidv4 } from "uuid";
import { assert } from "../utils/assert";
@ -94,22 +95,12 @@ export class MockAgent extends MockClient {
}
public async createInitialDocuments(count: number): Promise<void> {
this.client.logger.info(`Creating ${count} initial documents`);
for (let i = 0; i < count; i++) {
const file = `initial-${i}.md`;
this.doNotTouchWhileOffline.push(file);
const content = this.getContent();
this.client.logger.info(
`Creating initial file ${file} with content ${content}`
);
await this.create(file, new TextEncoder().encode(` ${content} `), {
ignoreSlowFileEvents: true
});
this.files.set(file, new TextEncoder().encode(` ${content} `));
}
// Wait for all initial documents to sync
await this.client.waitUntilFinished();
this.client.logger.info(`Initial documents created and synced`);
}
public async waitUntilSynced(): Promise<void> {
@ -159,7 +150,7 @@ export class MockAgent extends MockClient {
JSON.stringify(this.data, null, 2)
);
this.client.logger.info(
JSON.stringify(this.localFiles, null, 2)
JSON.stringify(this.files, null, 2)
);
throw error;
}
@ -192,14 +183,14 @@ export class MockAgent extends MockClient {
}
public assertFileSystemsAreConsistent(otherAgent: MockAgent): void {
const globalFiles = Array.from(otherAgent.localFiles.keys());
const localFiles = Array.from(this.localFiles.keys());
const globalFiles = Array.from(otherAgent.files.keys());
const localFiles = Array.from(this.files.keys());
const missingInOther = localFiles.filter(
(file) => !otherAgent.localFiles.has(file)
(file) => !otherAgent.files.has(file)
);
const missingInLocal = globalFiles.filter(
(file) => !this.localFiles.has(file)
(file) => !this.files.has(file)
);
try {
@ -214,10 +205,10 @@ export class MockAgent extends MockClient {
for (const file of globalFiles) {
const localContent = new TextDecoder().decode(
this.localFiles.get(file)
this.files.get(file)
);
const otherContent = new TextDecoder().decode(
otherAgent.localFiles.get(file)
otherAgent.files.get(file)
);
assert(
localContent === otherContent,
@ -229,15 +220,13 @@ export class MockAgent extends MockClient {
"Local data: " + JSON.stringify(this.data, null, 2)
);
this.client.logger.info(
"Local files: " +
Array.from(otherAgent.localFiles.keys()).join(", ")
"Local files: " + Array.from(otherAgent.files.keys()).join(", ")
);
otherAgent.client.logger.info(
"Local data: " + JSON.stringify(otherAgent.data, null, 2)
);
otherAgent.client.logger.info(
"Local files: " +
Array.from(otherAgent.localFiles.keys()).join(", ")
"Local files: " + Array.from(otherAgent.files.keys()).join(", ")
);
throw e;
@ -254,9 +243,9 @@ export class MockAgent extends MockClient {
}
for (const content of this.writtenContents) {
const found = Array.from(this.localFiles.keys()).filter((key) => {
const found = Array.from(this.files.keys()).filter((key) => {
return new TextDecoder()
.decode(this.localFiles.get(key))
.decode(this.files.get(key))
.includes(content);
});
@ -278,7 +267,7 @@ export class MockAgent extends MockClient {
const [file] = found;
const fileContent = new TextDecoder().decode(
this.localFiles.get(file)
this.files.get(file)
);
assert(
fileContent.split(content).length == 2,

View file

@ -2,13 +2,12 @@ import type { StoredDatabase, TextWithCursors } from "sync-client";
import { assert } from "../utils/assert";
import {
type RelativePath,
type FileSystemOperations,
type SyncSettings,
SyncClient
SyncClient,
debugging
} from "sync-client";
export class MockClient implements FileSystemOperations {
protected readonly localFiles = new Map<string, Uint8Array>();
export class MockClient extends debugging.InMemoryFileSystem {
protected client!: SyncClient;
protected data: Partial<{
@ -20,6 +19,7 @@ export class MockClient implements FileSystemOperations {
initialSettings: Partial<SyncSettings>,
protected readonly useSlowFileEvents: boolean
) {
super();
this.data.settings = initialSettings;
}
@ -40,28 +40,6 @@ export class MockClient implements FileSystemOperations {
await this.client.start();
}
public async listFilesRecursively(
_root: RelativePath | undefined = undefined // we don't use multi-level paths during tests
): Promise<RelativePath[]> {
return Array.from(this.localFiles.keys());
}
public async read(path: RelativePath): Promise<Uint8Array> {
const file = this.localFiles.get(path);
if (!file) {
throw new Error(`File ${path} does not exist`);
}
return file;
}
public async getFileSize(path: RelativePath): Promise<number> {
return (await this.read(path)).length;
}
public async exists(path: RelativePath): Promise<boolean> {
return this.localFiles.has(path);
}
public async create(
path: RelativePath,
newContent: Uint8Array,
@ -69,13 +47,13 @@ export class MockClient implements FileSystemOperations {
ignoreSlowFileEvents: false
}
): Promise<void> {
if (this.localFiles.has(path)) {
if (this.files.has(path)) {
throw new Error(`File ${path} already exists`);
}
this.client.logger.info(
`Creating file ${path} with content ${new TextDecoder().decode(newContent)}`
);
this.localFiles.set(path, newContent);
this.files.set(path, newContent);
this.executeFileOperation(
async () => this.client.syncLocallyCreatedFile(path),
@ -83,25 +61,21 @@ export class MockClient implements FileSystemOperations {
);
}
public async createDirectory(_path: RelativePath): Promise<void> {
// This doesn't mean anything in our virtual FS representation
}
public async atomicUpdateText(
public override async atomicUpdateText(
path: RelativePath,
updater: (currentContent: TextWithCursors) => TextWithCursors,
{ ignoreSlowFileEvents }: { ignoreSlowFileEvents: boolean } = {
ignoreSlowFileEvents: false
}
): Promise<string> {
const file = this.localFiles.get(path);
const file = this.files.get(path);
if (!file) {
throw new Error(`File ${path} does not exist`);
}
const currentContent = new TextDecoder().decode(file);
const newContent = updater({ text: currentContent, cursors: [] }).text;
const newContentUint8Array = new TextEncoder().encode(newContent);
this.localFiles.set(path, newContentUint8Array);
this.files.set(path, newContentUint8Array);
if (!this.useSlowFileEvents) {
const existingParts = currentContent
@ -109,13 +83,13 @@ export class MockClient implements FileSystemOperations {
.map((part) => part.trim());
const newParts = newContent.split(" ").map((part) => part.trim());
existingParts.forEach((part) =>
// all changes should be additive
{
assert(
newParts.includes(part),
`Part ${part} not found in new content: ${newContent}`
);
}
// all changes should be additive
{
assert(
newParts.includes(part),
`Part ${part} not found in new content: ${newContent}`
);
}
);
}
@ -134,9 +108,12 @@ export class MockClient implements FileSystemOperations {
return newContent;
}
public async write(path: RelativePath, content: Uint8Array): Promise<void> {
const hasExisted = this.localFiles.has(path);
this.localFiles.set(path, content);
public override async write(
path: RelativePath,
content: Uint8Array
): Promise<void> {
const hasExisted = this.files.has(path);
this.files.set(path, content);
this.client.logger.info(
`Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}`
@ -153,16 +130,16 @@ export class MockClient implements FileSystemOperations {
});
}
public async delete(
public override async delete(
path: RelativePath,
{ ignoreSlowFileEvents }: { ignoreSlowFileEvents: boolean } = {
ignoreSlowFileEvents: false
}
): Promise<void> {
this.client.logger.info(
`Deleting file: ${path} with:\n content ${new TextDecoder().decode(this.localFiles.get(path))}`
`Deleting file: ${path} with:\n content ${new TextDecoder().decode(this.files.get(path))}`
);
this.localFiles.delete(path);
this.files.delete(path);
this.executeFileOperation(
async () => this.client.syncLocallyDeletedFile(path),
@ -170,20 +147,20 @@ export class MockClient implements FileSystemOperations {
);
}
public async rename(
public override async rename(
oldPath: RelativePath,
newPath: RelativePath,
{ ignoreSlowFileEvents }: { ignoreSlowFileEvents: boolean } = {
ignoreSlowFileEvents: false
}
): Promise<void> {
const file = this.localFiles.get(oldPath);
const file = this.files.get(oldPath);
if (!file) {
throw new Error(`File ${oldPath} does not exist`);
}
this.localFiles.set(newPath, file);
this.files.set(newPath, file);
if (oldPath !== newPath) {
this.localFiles.delete(oldPath);
this.files.delete(oldPath);
}
this.client.logger.info(

View file

@ -6,7 +6,7 @@ import { v4 as uuidv4 } from "uuid";
import { randomCasing } from "./utils/random-casing";
const TEST_ITERATIONS = 5;
const MAX_INITIAL_DOCS = 5;
const MAX_INITIAL_DOCS = 0;
// Simulate async file access by injecting waiting time before returning from file operations.
let slowFileEvents = false;
@ -65,8 +65,6 @@ async function runTest({
}
try {
await utils.awaitAll(clients.map(async (client) => client.init()));
for (const client of clients) {
const initialDocCount = Math.floor(
Math.random() * MAX_INITIAL_DOCS
@ -79,6 +77,10 @@ async function runTest({
}
}
await utils.awaitAll(clients.map(async (client) => client.init()));
for (let i = 0; i < iterations; i++) {
logger.info(`Iteration ${i + 1}/${iterations}`);
await utils.awaitAll(clients.map(async (client) => client.act()));
@ -217,5 +219,8 @@ runTests()
})
.catch((error: unknown) => {
logger.error(`Error - tests failed with ${error}`);
if (error instanceof Error && error.stack) {
logger.error(error.stack);
}
process.exit(1);
});