Skip to content

Commit

Permalink
Merge pull request #307 from bijela-gora/tests/for-nodefs-storage
Browse files Browse the repository at this point in the history
Tests suite for storage adapters
  • Loading branch information
HerbCaudill authored Mar 27, 2024
2 parents c805fae + 847bbcd commit 32fd1dc
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PeerId } from "@automerge/automerge-repo"
import { describe, it } from "vitest"
import {
runAdapterTests,
runNetworkAdapterTests,
type SetupFn,
} from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
import { BroadcastChannelNetworkAdapter } from "../src/index.js"
Expand All @@ -15,7 +15,7 @@ describe("BroadcastChannel", () => {
return { adapters: [a, b, c] }
}

runAdapterTests(setup)
runNetworkAdapterTests(setup)

it("allows a channel name to be specified in the options and limits messages to that channel", async () => {
const a = new BroadcastChannelNetworkAdapter()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe } from "vitest"
import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
import { runNetworkAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
import { MessageChannelNetworkAdapter as Adapter } from "../src/index.js"

// bob is the hub, alice and charlie are spokes
describe("MessageChannelNetworkAdapter", () => {
runAdapterTests(async () => {
runNetworkAdapterTests(async () => {
const aliceBobChannel = new MessageChannel()
const bobCharlieChannel = new MessageChannel()

Expand All @@ -24,7 +24,7 @@ describe("MessageChannelNetworkAdapter", () => {
}, "hub and spoke")

// all 3 peers connected directly to each other
runAdapterTests(async () => {
runNetworkAdapterTests(async () => {
const aliceBobChannel = new MessageChannel()
const bobCharlieChannel = new MessageChannel()
const aliceCharlieChannel = new MessageChannel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { generateAutomergeUrl } from "@automerge/automerge-repo/dist/AutomergeUrl"
import { eventPromise } from "@automerge/automerge-repo/src/helpers/eventPromise"
import { headsAreSame } from "@automerge/automerge-repo/src/helpers/headsAreSame.js"
import { runAdapterTests } from "@automerge/automerge-repo/src/helpers/tests/network-adapter-tests.js"
import { runNetworkAdapterTests } from "@automerge/automerge-repo/src/helpers/tests/network-adapter-tests.js"
import { DummyStorageAdapter } from "@automerge/automerge-repo/test/helpers/DummyStorageAdapter.js"
import assert from "assert"
import * as CBOR from "cbor-x"
Expand All @@ -27,7 +27,7 @@ describe("Websocket adapters", () => {
const serverPeerId = "server" as PeerId
const documentId = parseAutomergeUrl(generateAutomergeUrl()).documentId

runAdapterTests(async () => {
runNetworkAdapterTests(async () => {
const {
clients: [aliceAdapter, bobAdapter],
server,
Expand Down
3 changes: 2 additions & 1 deletion packages/automerge-repo-storage-nodefs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"watch": "npm-watch build"
"watch": "npm-watch build",
"test": "vitest"
},
"dependencies": {
"@automerge/automerge-repo": "workspace:*",
Expand Down
14 changes: 7 additions & 7 deletions packages/automerge-repo-storage-nodefs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ export class NodeFSStorageAdapter implements StorageAdapterInterface {

// The "keys" in the cache don't include the baseDirectory.
// We want to de-dupe with the cached keys so we'll use getKey to normalize them.
const diskKeys: string[] = diskFiles.map((fileName: string) =>
getKey([path.relative(this.baseDirectory, fileName)])
)
const diskKeys: string[] = diskFiles.map((fileName: string) => {
const k = getKey([path.relative(this.baseDirectory, fileName)])
return k.slice(0, 2) + k.slice(3)
})

// Combine and deduplicate the lists of keys
const allKeys = [...new Set([...cachedKeys, ...diskKeys])]
Expand Down Expand Up @@ -113,13 +114,12 @@ export class NodeFSStorageAdapter implements StorageAdapterInterface {

private getFilePath(keyArray: string[]): string {
const [firstKey, ...remainingKeys] = keyArray
const firstKeyDir = path.join(
return path.join(
this.baseDirectory,
firstKey.slice(0, 2),
firstKey.slice(2)
firstKey.slice(2),
...remainingKeys
)

return path.join(firstKeyDir, ...remainingKeys)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
import { describe } from "vitest"
import { runStorageAdapterTests } from "../../automerge-repo/src/helpers/tests/storage-adapter-tests"
import { NodeFSStorageAdapter } from "../src"

describe("NodeFSStorageAdapter", () => {
const setup = async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "automerge-repo-tests"))
const teardown = () => {
fs.rmSync(dir, { force: true, recursive: true })
}
const adapter = new NodeFSStorageAdapter(dir)
return { adapter, teardown }
}

runStorageAdapterTests(setup)
})
1 change: 0 additions & 1 deletion packages/automerge-repo-storage-nodefs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "react",
"module": "NodeNext",
"moduleResolution": "Node16",
"declaration": true,
Expand Down
11 changes: 11 additions & 0 deletions packages/automerge-repo-storage-nodefs/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig, mergeConfig } from "vitest/config"
import rootConfig from "../../vitest.config"

export default mergeConfig(
rootConfig,
defineConfig({
test: {
environment: "node",
},
})
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { pause } from "../pause.js"
* - `teardown`: An optional function that will be called after the tests have run. This can be used
* to clean up any resources that were created during the test.
*/
export function runAdapterTests(_setup: SetupFn, title?: string): void {
export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
// Wrap the provided setup function
const setup = async () => {
const { adapters, teardown = NO_OP } = await _setup()
Expand All @@ -28,7 +28,9 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
return { adapters: [a, b, c], teardown }
}

describe(`Adapter acceptance tests ${title ? `(${title})` : ""}`, () => {
describe(`Network adapter acceptance tests ${
title ? `(${title})` : ""
}`, () => {
it("can sync 2 repos", async () => {
const doTest = async (
a: NetworkAdapterInterface[],
Expand Down
193 changes: 193 additions & 0 deletions packages/automerge-repo/src/helpers/tests/storage-adapter-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { describe, expect, it } from "vitest"

import type { StorageAdapterInterface } from "../../storage/StorageAdapterInterface.js"

const PAYLOAD_A = () => new Uint8Array([0, 1, 127, 99, 154, 235])
const PAYLOAD_B = () => new Uint8Array([1, 76, 160, 53, 57, 10, 230])
const PAYLOAD_C = () => new Uint8Array([2, 111, 74, 131, 236, 96, 142, 193])

const LARGE_PAYLOAD = new Uint8Array(100000).map(() => Math.random() * 256)

export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
const setup = async () => {
const { adapter, teardown = NO_OP } = await _setup()
return { adapter, teardown }
}

describe(`Network adapter acceptance tests ${
title ? `(${title})` : ""
}`, () => {
describe("load", () => {
it("should return undefined if there is no data", async () => {
const { adapter, teardown } = await setup()

const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
expect(actual).toBeUndefined()

teardown()
})
})

describe("save and load", () => {
it("should return data that was saved", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["storage-adapter-id"], PAYLOAD_A())
const actual = await adapter.load(["storage-adapter-id"])
expect(actual).toStrictEqual(PAYLOAD_A())

teardown()
})

it("should work with composite keys", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
expect(actual).toStrictEqual(PAYLOAD_A())

teardown()
})

it("should work with a large payload", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], LARGE_PAYLOAD)
const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
expect(actual).toStrictEqual(LARGE_PAYLOAD)

teardown()
})
})

describe("loadRange", () => {
it("should return an empty array if there is no data", async () => {
const { adapter, teardown } = await setup()

expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([])

teardown()
})
})

describe("save and loadRange", () => {
it("should return all the data that matches the key", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())

expect(await adapter.loadRange(["AAAAA"])).toStrictEqual(
expect.arrayContaining([
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
{ key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
{ key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
])
)

expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual(
expect.arrayContaining([
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
{ key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
])
)

teardown()
})

it("should only load values that match they key", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_C())

const actual = await adapter.loadRange(["AAAAA"])
expect(actual).toStrictEqual(
expect.arrayContaining([
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
])
)
expect(actual).toStrictEqual(
expect.not.arrayContaining([
{ key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_C() },
])
)

teardown()
})
})

describe("save and remove", () => {
it("after removing, should be empty", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "snapshot", "xxxxx"], PAYLOAD_A())
await adapter.remove(["AAAAA", "snapshot", "xxxxx"])

expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([])
expect(
await adapter.load(["AAAAA", "snapshot", "xxxxx"])
).toBeUndefined()

teardown()
})
})

describe("save and save", () => {
it("should overwrite data saved with the same key", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_B())

expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual([
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_B() },
])

teardown()
})
})

describe("removeRange", () => {
it("should remove a range of records", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())

await adapter.removeRange(["AAAAA", "sync-state"])

expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([
{ key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
])

teardown()
})

it("should not remove records that don't match", async () => {
const { adapter, teardown } = await setup()

await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_B())

await adapter.removeRange(["AAAAA"])

const actual = await adapter.loadRange(["BBBBB"])
expect(actual).toStrictEqual([
{ key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_B() },
])

teardown()
})
})
})
}

const NO_OP = () => {}

export type SetupFn = () => Promise<{
adapter: StorageAdapterInterface
teardown?: () => void
}>
11 changes: 11 additions & 0 deletions packages/automerge-repo/test/DummyStorageAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { beforeEach, describe } from "vitest"
import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
import { runStorageAdapterTests } from "../src/helpers/tests/storage-adapter-tests.js"

describe("DummyStorageAdapter", () => {
const setup = async () => ({
adapter: new DummyStorageAdapter(),
})

runStorageAdapterTests(setup, "DummyStorageAdapter")
})

0 comments on commit 32fd1dc

Please sign in to comment.