Skip to content

Commit

Permalink
feat: Restore test coverage of updates (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
stbrody authored May 29, 2024
1 parent 175cfe1 commit 53248f6
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 78 deletions.
17 changes: 8 additions & 9 deletions suite/src/__tests__/fast/anchor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AnchorStatus, StreamUtils } from '@ceramicnetwork/common'
import { TileDocument } from '@ceramicnetwork/stream-tile'
import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'
import { DateTime } from 'luxon'

Expand All @@ -18,8 +17,8 @@ describe('anchor', () => {

for (const req of anchorReqs) {
const ceramic = await newCeramic(ComposeDbUrls[0])
const tile = await TileDocument.load(ceramic, <string>req.StreamId.S)
console.log(`${tile.id}: anchor status = ${AnchorStatus[tile.state.anchorStatus]}`)
const doc = await ceramic.loadStream(<string>req.StreamId.S)
console.log(`${doc.id}: anchor status = ${AnchorStatus[doc.state.anchorStatus]}`)
const now = DateTime.utc()
const createdAt = DateTime.fromSeconds(parseInt(<string>req.Creation.N))
const deltaMinutes = now.diff(createdAt).as('minutes')
Expand All @@ -29,28 +28,28 @@ describe('anchor', () => {
//
// Don't explicitly check for failures until a timeout because failed requests can be retried and successful
// on subsequent attempts within the configured interval.
if (tile.state.anchorStatus == AnchorStatus.ANCHORED || deltaMinutes >= configMinutes) {
if (doc.state.anchorStatus == AnchorStatus.ANCHORED || deltaMinutes >= configMinutes) {
try {
if (tile.state.anchorStatus != AnchorStatus.ANCHORED) {
if (doc.state.anchorStatus != AnchorStatus.ANCHORED) {
// If the stream wasn't already anchored, make sure we haven't been waiting too long. This check
// will also catch anchor failures (i.e. requests for which anchoring was attempted but failed
// even after retries).
expect(deltaMinutes).toBeLessThan(configMinutes)
}

await helpers.markStreamReqAsAnchored(tile.id)
await helpers.markStreamReqAsAnchored(doc.id)
} catch (err) {
console.error(
`Test failed. StreamID: ${tile.id.toString()}, state:\n${JSON.stringify(
StreamUtils.serializeState(tile.state),
`Test failed. StreamID: ${doc.id.toString()}, state:\n${JSON.stringify(
StreamUtils.serializeState(doc.state),
null,
2,
)}`,
)

// If the anchoring failed, we don't want to leave this stream in the database,
// as it will cause all future test executions to fail as well.
await helpers.deleteStreamReq(tile.id)
await helpers.deleteStreamReq(doc.id)

throw err
}
Expand Down
23 changes: 11 additions & 12 deletions suite/src/__tests__/fast/longevity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AnchorStatus, StreamUtils } from '@ceramicnetwork/common'
import { StreamID } from '@ceramicnetwork/streamid'
import { TileDocument } from '@ceramicnetwork/stream-tile'
import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'

import { newCeramic } from '../../utils/ceramicHelpers.js'
Expand Down Expand Up @@ -34,43 +33,43 @@ describe('longevity', () => {
const ceramic = await newCeramic(apiUrl)
for (const streamId of streamIds) {
console.log(`Loading stream ${streamId} on ${apiUrl}`)
const tile = await TileDocument.load(ceramic, streamId)
const doc = await ceramic.loadStream(streamId)
try {
expect(tile.content).toEqual(expectedContent)
expect(tile.state.anchorStatus).toEqual(AnchorStatus.ANCHORED)
expect(doc.content).toEqual(expectedContent)
expect(doc.state.anchorStatus).toEqual(AnchorStatus.ANCHORED)

// Now load the same stream but at a specific CommitID. Loading at a CommitID
// means the Ceramic node can't get the state from the state store, but has
// to actually load and apply the commits from IPFS, so it lets us check that the
// underlying ipfs blocks are still available.
const commitIds = tile.allCommitIds
const commitIds = doc.allCommitIds
const prevCommitId = commitIds[commitIds.length - 2]
console.log(
`Loading commit ${prevCommitId} on ${apiUrl} for stream state:\n${JSON.stringify(
StreamUtils.serializeState(tile.state),
StreamUtils.serializeState(doc.state),
null,
2,
)}`,
)
const tileAtPrevCommitId = await TileDocument.load(ceramic, prevCommitId)
const docAtPrevCommitId = await ceramic.loadStream(prevCommitId)

// The last commit is an anchor commit, so the second to last commit will actually
// have the same content as the current state with the most recent commit, it just
// won't have the anchor information.
expect(tileAtPrevCommitId.content).toEqual(expectedContent)
expect(tileAtPrevCommitId.state.anchorStatus).not.toEqual(AnchorStatus.ANCHORED)
expect(docAtPrevCommitId.content).toEqual(expectedContent)
expect(docAtPrevCommitId.state.anchorStatus).not.toEqual(AnchorStatus.ANCHORED)
} catch (err) {
console.error(
`Test failed. StreamID: ${tile.id.toString()}, state:\n${JSON.stringify(
StreamUtils.serializeState(tile.state),
`Test failed. StreamID: ${doc.id.toString()}, state:\n${JSON.stringify(
StreamUtils.serializeState(doc.state),
null,
2,
)}`,
)

// If the test failed, we don't want to leave this stream in the database,
// as it will cause all future test executions to fail as well.
await helpers.deleteStreamReq(tile.id)
await helpers.deleteStreamReq(doc.id)

throw err
}
Expand Down
34 changes: 19 additions & 15 deletions suite/src/__tests__/fast/model-correctness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import { createDid } from '../../utils/didHelper.js'
import { StreamID } from '@ceramicnetwork/streamid'
import { Model } from '@ceramicnetwork/stream-model'
import { ModelInstanceDocument } from '@ceramicnetwork/stream-model-instance'
import { newModel, basicModelDocumentContent } from '../../models/modelConstants'
import { newModel } from '../../models/modelConstants'
import { CeramicClient } from '@ceramicnetwork/http-client'
import { CommonTestUtils as TestUtils } from '@ceramicnetwork/common-test-utils'
import { loadDocumentOrTimeout, waitForIndexingOrTimeout } from '../../utils/composeDbHelpers.js'
import { indexModelOnNode, loadDocumentOrTimeout } from '../../utils/composeDbHelpers.js'

const ComposeDbUrls = String(process.env.COMPOSEDB_URLS).split(',')
const adminSeeds = String(process.env.COMPOSEDB_ADMIN_DID_SEEDS).split(',')
const nodeSyncWaitTimeSec = 5
const indexWaitTimeMin = 1

describe('Model Integration Test', () => {
let ceramicNode1: CeramicClient
Expand All @@ -28,31 +27,36 @@ describe('Model Integration Test', () => {
ceramicNode1 = await newCeramic(ComposeDbUrls[0], did1)
ceramicNode2 = await newCeramic(ComposeDbUrls[1], did2)
const model = await Model.create(ceramicNode1, newModel)
await TestUtils.waitForConditionOrTimeout(async () =>
ceramicNode2
.loadStream(model.id)
.then((_) => true)
.catch((_) => false),
)
await ceramicNode1.admin.startIndexingModels([model.id])
await ceramicNode2.admin.startIndexingModels([model.id])
modelId = model.id
await waitForIndexingOrTimeout(ceramicNode1, modelId, 1000 * 60 * indexWaitTimeMin)
await waitForIndexingOrTimeout(ceramicNode2, modelId, 1000 * 60 * indexWaitTimeMin)

await indexModelOnNode(ceramicNode1, model.id)
await indexModelOnNode(ceramicNode2, model.id)
})

test('Create a ModelInstanceDocument on one node and read it from another', async () => {
test('ModelInstanceDocuments sync between nodes', async () => {
const modelInstanceDocumentMetadata = { model: modelId }
const document1 = await ModelInstanceDocument.create(
ceramicNode1,
basicModelDocumentContent,
{ step: 1 },
modelInstanceDocumentMetadata,
{ anchor: false },
)
const document2 = await loadDocumentOrTimeout(
ceramicNode2,
document1.id,
1000 * nodeSyncWaitTimeSec,
)
expect(document2.id).toEqual(document1.id)
expect(document1.content).toEqual(document2.content)

// Now update on the second node and ensure update syncs back to the first node.
await document2.replace({ step: 2 }, null, { anchor: false })
await TestUtils.waitForConditionOrTimeout(async () => {
await document1.sync()
return document1.content?.step == 2
})
expect(document1.content).toEqual(document2.content)
expect(document1.state.log.length).toEqual(2)
expect(document2.state.log.length).toEqual(2)
})
})
82 changes: 58 additions & 24 deletions suite/src/__tests__/fast/update.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,55 @@
import { StreamReaderWriter, SyncOptions } from '@ceramicnetwork/common'
import CeramicClient from '@ceramicnetwork/http-client'
import { TileDocument } from '@ceramicnetwork/stream-tile'
import { afterAll, beforeAll, expect, test, describe } from '@jest/globals'

import * as helpers from '../../utils/dynamoDbHelpers.js'
import { utilities } from '../../utils/common.js'
import { newCeramic, metadata } from '../../utils/ceramicHelpers.js'
import { newCeramic } from '../../utils/ceramicHelpers.js'
import { createDid } from '../../utils/didHelper.js'
import { newModel } from '../../models/modelConstants.js'
import { Model } from '@ceramicnetwork/stream-model'
import { indexModelOnNode } from '../../utils/composeDbHelpers.js'
import { StreamID } from '@ceramicnetwork/streamid'
import { ModelInstanceDocument } from '@ceramicnetwork/stream-model-instance'

const delay = utilities.delay

// Environment variables
const ComposeDbUrls = String(process.env.COMPOSEDB_URLS).split(',')
const composeDbUrls = String(process.env.COMPOSEDB_URLS).split(',')
const adminSeeds = String(process.env.COMPOSEDB_ADMIN_DID_SEEDS).split(',')

///////////////////////////////////////////////////////////////////////////////
/// Create/Update Tests
///////////////////////////////////////////////////////////////////////////////

describe.skip('update', () => {
beforeAll(async () => await helpers.createTestTable())
let modelId: StreamID

describe('update', () => {
beforeAll(async () => {
await helpers.createTestTable()

if (adminSeeds.length < composeDbUrls.length) {
throw new Error(
`Must provide an admin DID seed for each node. Number of nodes: ${composeDbUrls.length}, number of seeds: ${adminSeeds.length}`,
)
}

const did0 = await createDid(adminSeeds[0])
const ceramicNode0 = await newCeramic(composeDbUrls[0], did0)
const model = await Model.create(ceramicNode0, newModel)
modelId = model.id
await indexModelOnNode(ceramicNode0, model.id)

for (let i = 1; i < composeDbUrls.length; i++) {
const did = await createDid(adminSeeds[i])
const node = await newCeramic(composeDbUrls[i], did)
await indexModelOnNode(node, model.id)
}
})
afterAll(async () => await helpers.cleanup())

// Run tests with each node being the node where a stream is created
generateUrlCombinations(ComposeDbUrls).forEach(testUpdate)
generateUrlCombinations(composeDbUrls).forEach(testUpdate)
})

function generateUrlCombinations(urls: string[]): string[][] {
Expand All @@ -33,42 +61,48 @@ function testUpdate(composeDbUrls: string[]) {
const firstNodeUrl = composeDbUrls[0]
const content = { step: 0 }
let firstCeramic: CeramicClient.CeramicClient
let firstTile: TileDocument
let firstDocument: ModelInstanceDocument

// Create and update on first node
test(`create stream on ${firstNodeUrl}`, async () => {
firstCeramic = await newCeramic(firstNodeUrl)
firstTile = await TileDocument.create(firstCeramic as StreamReaderWriter, content, metadata, {
anchor: false,
})
const metadata = { controllers: [firstCeramic.did!.id], model: modelId }
firstDocument = await ModelInstanceDocument.create(
firstCeramic as StreamReaderWriter,
content,
metadata,
{
anchor: false,
},
)
console.log(
`Created stream on ${firstNodeUrl}: ${firstTile.id.toString()} with step ${content.step}`,
`Created stream on ${firstNodeUrl}: ${firstDocument.id.toString()} with step ${content.step}`,
)
})

test(`update stream on ${firstNodeUrl}`, async () => {
content.step++
await firstTile.update(content, undefined, { anchor: false })
await firstDocument.replace(content, undefined, { anchor: false })
console.log(
`Updated stream on ${firstNodeUrl}: ${firstTile.id.toString()} with step ${content.step}`,
`Updated stream on ${firstNodeUrl}: ${firstDocument.id.toString()} with step ${content.step}`,
)
})

// Test load, update, and sync on subsequent node(s)
// Skip first url because it was already handled in the previous tests
for (let idx = 1; idx < composeDbUrls.length; idx++) {
const apiUrl = composeDbUrls[idx]
let tile: TileDocument
let doc: ModelInstanceDocument
test(`load stream on ${apiUrl}`, async () => {
await delay(5)
const ceramic = await newCeramic(apiUrl)
console.log(
`Loading stream ${firstTile.id.toString()} on ${apiUrl} with step ${content.step}`,
`Loading stream ${firstDocument.id.toString()} on ${apiUrl} with step ${content.step}`,
)
tile = await TileDocument.load(ceramic, firstTile.id)
expect(tile.content).toEqual(content)
doc = await ModelInstanceDocument.load(ceramic, firstDocument.id)
expect(doc.content).toEqual(content)
console.log(
`Loaded stream on ${apiUrl}: ${firstTile.id.toString()} successfully with step ${
`Loaded stream on ${apiUrl}: ${firstDocument.id.toString()} successfully with step ${
content.step
}`,
)
Expand All @@ -81,25 +115,25 @@ function testUpdate(composeDbUrls: string[]) {
// Update on first node and wait for update to propagate to other nodes via pubsub
// Only anchor on the final write to avoid writes conflicting with anchors.
console.log(
`Updating stream ${firstTile.id.toString()} on ${firstNodeUrl} so we can sync it on ${apiUrl} with step ${
`Updating stream ${firstDocument.id.toString()} on ${firstNodeUrl} so we can sync it on ${apiUrl} with step ${
content.step
}`,
)
await firstTile.update(content, undefined, { anchor: isFinalWriter })
await firstDocument.replace(content, undefined, { anchor: isFinalWriter })
console.log(`Updating complete, sleeping 5 seconds before syncing`)
await delay(5)
console.log(`Sleep complete, syncing`)
await tile.sync({ sync: SyncOptions.NEVER_SYNC })
expect(tile.content).toEqual(firstTile.content)
await doc.sync({ sync: SyncOptions.NEVER_SYNC })
expect(doc.content).toEqual(firstDocument.content)
console.log(
`Synced stream on ${apiUrl}: ${firstTile.id.toString()} successfully with step ${
`Synced stream on ${apiUrl}: ${firstDocument.id.toString()} successfully with step ${
content.step
}`,
)

if (isFinalWriter) {
// Store the anchor request in the DB
await helpers.storeStreamReq(firstTile.id)
await helpers.storeStreamReq(firstDocument.id)
}
})
}
Expand Down
6 changes: 1 addition & 5 deletions suite/src/models/modelConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const newModel: ModelDefinition = {
type: 'object',
additionalProperties: false,
properties: {
myData: {
step: {
type: 'integer',
minimum: 0,
maximum: 10000,
Expand All @@ -18,7 +18,3 @@ export const newModel: ModelDefinition = {
type: 'list',
},
}

export const basicModelDocumentContent = {
myData: 2,
}
Loading

0 comments on commit 53248f6

Please sign in to comment.