189 lines
4.5 KiB
TypeScript
189 lines
4.5 KiB
TypeScript
import type { SyncSettings } from "sync-client";
|
|
import { MockAgent } from "./agent/mock-agent";
|
|
import { sleep } from "./utils/sleep";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { randomCasing } from "./utils/random-casing";
|
|
|
|
const TEST_ITERATIONS = 5;
|
|
|
|
// Simulate async file access by injecting waiting time before returning from file operations.
|
|
let slowFileEvents = false;
|
|
|
|
async function runTest({
|
|
agentCount,
|
|
concurrency,
|
|
iterations,
|
|
doDeletes,
|
|
doResets,
|
|
useSlowFileEvents,
|
|
jitterScaleInSeconds
|
|
}: {
|
|
agentCount: number;
|
|
concurrency: number;
|
|
iterations: number;
|
|
doDeletes: boolean;
|
|
doResets: boolean;
|
|
useSlowFileEvents: boolean;
|
|
jitterScaleInSeconds: number;
|
|
}): Promise<void> {
|
|
slowFileEvents = useSlowFileEvents;
|
|
|
|
const settings = `with ${agentCount} agents, concurrency ${concurrency}, iterations ${iterations}, doDeletes ${doDeletes}, doResets ${doResets}, jitterScaleInSeconds ${jitterScaleInSeconds}, useSlowFileEvents ${useSlowFileEvents}`;
|
|
console.info(`Running test ${settings}`);
|
|
|
|
const vaultName = uuidv4();
|
|
console.info(`Using vault name: ${vaultName}`);
|
|
const initialSettings: Partial<SyncSettings> = {
|
|
isSyncEnabled: true,
|
|
token: " test-token-change-me ", // same as in sync-server/config-e2e.yml with spaces
|
|
vaultName: randomCasing(vaultName) + (Math.random() > 0.5 ? " " : ""), // extra spaces shouldn't matter
|
|
syncConcurrency: concurrency,
|
|
remoteUri: "http://localhost:3000"
|
|
};
|
|
|
|
const clients: MockAgent[] = [];
|
|
for (let i = 0; i < agentCount; i++) {
|
|
clients.push(
|
|
new MockAgent(
|
|
initialSettings,
|
|
`agent-${i}`,
|
|
doDeletes,
|
|
doResets,
|
|
useSlowFileEvents,
|
|
jitterScaleInSeconds
|
|
)
|
|
);
|
|
}
|
|
|
|
try {
|
|
// eslint-disable-next-line no-restricted-properties
|
|
await Promise.all(clients.map(async (client) => client.init()));
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
console.info(`Iteration ${i + 1}/${iterations}`);
|
|
// eslint-disable-next-line no-restricted-properties
|
|
await Promise.all(clients.map(async (client) => client.act()));
|
|
await sleep(100);
|
|
}
|
|
|
|
console.info("Stopping agents");
|
|
|
|
// Each agent can have unpushed changes which might conflict with eachother so each has to resolve the conflicts & push, and
|
|
for (const client of clients) {
|
|
try {
|
|
await client.finish();
|
|
} catch (err) {
|
|
if (!slowFileEvents) {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
// then we need a second pass to ensure that all agents pull the same state.
|
|
for (const client of clients) {
|
|
try {
|
|
await client.destroy();
|
|
} catch (err) {
|
|
if (!slowFileEvents) {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.info("Agents finished successfully");
|
|
|
|
clients.slice(0, -1).forEach((client, i) => {
|
|
console.info(
|
|
`Checking consistency between ${client.name} and ${clients[i + 1].name}`
|
|
);
|
|
client.assertFileSystemsAreConsistent(clients[i]);
|
|
console.info(`Consistency check for ${client.name} passed`);
|
|
});
|
|
|
|
console.info("File systems found to be consistent");
|
|
|
|
clients.forEach((client) => {
|
|
console.info(`Checking content for ${client.name}`);
|
|
client.assertAllContentIsPresentOnce();
|
|
console.info(`Content check for ${client.name} passed`);
|
|
});
|
|
|
|
console.info(`Test passed ${settings}`);
|
|
} catch (err) {
|
|
console.error(`Test failed ${settings}`);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function runTests(): Promise<void> {
|
|
for (let i = 0; i < TEST_ITERATIONS; i++) {
|
|
await runTest({
|
|
agentCount: 2,
|
|
concurrency: 16,
|
|
iterations: 100,
|
|
doDeletes: true,
|
|
doResets: true,
|
|
useSlowFileEvents: true,
|
|
jitterScaleInSeconds: 0.75
|
|
});
|
|
|
|
for (const useSlowFileEvents of [false, true]) {
|
|
for (const concurrency of [
|
|
16,
|
|
1 // test with concurrency 1 to check for deadlocks
|
|
]) {
|
|
for (const doDeletes of [true, false]) {
|
|
await runTest({
|
|
agentCount: 2,
|
|
concurrency,
|
|
iterations: 100,
|
|
doDeletes,
|
|
doResets: false,
|
|
useSlowFileEvents,
|
|
jitterScaleInSeconds: 0.75
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
if (slowFileEvents) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
error instanceof Error &&
|
|
error.message.includes(
|
|
"WebSocket was closed before the connection was established"
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
console.error("Uncaught exception:", error);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on("unhandledRejection", (error, _promise) => {
|
|
if (error instanceof Error && error.message === "Sync was reset") {
|
|
return;
|
|
}
|
|
|
|
if (slowFileEvents) {
|
|
return;
|
|
}
|
|
|
|
console.error("Unhandled rejection:", error);
|
|
process.exit(1);
|
|
});
|
|
|
|
runTests()
|
|
.then(() => {
|
|
process.exit(0);
|
|
})
|
|
.catch((err: unknown) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|