-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #494 from vector-im/DanilaFe/backfill-changes
Unit tests for GapWriter, using a new timeline mock utility
- Loading branch information
Showing
8 changed files
with
463 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,6 +75,8 @@ export class NullLogItem { | |
|
||
refDetached() {} | ||
|
||
ensureRefId() {} | ||
|
||
get level() { | ||
return LogLevel; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -253,3 +253,176 @@ export class GapWriter { | |
return {entries, updatedEntries, fragments}; | ||
} | ||
} | ||
|
||
import {FragmentIdComparer} from "../FragmentIdComparer.js"; | ||
import {RelationWriter} from "./RelationWriter.js"; | ||
import {createMockStorage} from "../../../../mocks/Storage.js"; | ||
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; | ||
import {NullLogItem} from "../../../../logging/NullLogger.js"; | ||
import {TimelineMock, eventIds, eventId} from "../../../../mocks/TimelineMock.ts"; | ||
import {SyncWriter} from "./SyncWriter.js"; | ||
import {MemberWriter} from "./MemberWriter.js"; | ||
import {KeyLimits} from "../../../storage/common"; | ||
|
||
export function tests() { | ||
const roomId = "!room:hs.tdl"; | ||
const alice = "[email protected]"; | ||
const logger = new NullLogItem(); | ||
|
||
async function createGapFillTxn(storage) { | ||
return storage.readWriteTxn([ | ||
storage.storeNames.roomMembers, | ||
storage.storeNames.pendingEvents, | ||
storage.storeNames.timelineEvents, | ||
storage.storeNames.timelineRelations, | ||
storage.storeNames.timelineFragments, | ||
]); | ||
} | ||
|
||
async function setup() { | ||
const storage = await createMockStorage(); | ||
const txn = await createGapFillTxn(storage); | ||
const fragmentIdComparer = new FragmentIdComparer([]); | ||
const relationWriter = new RelationWriter({ | ||
roomId, fragmentIdComparer, ownUserId: alice, | ||
}); | ||
const gapWriter = new GapWriter({ | ||
roomId, storage, fragmentIdComparer, relationWriter | ||
}); | ||
const memberWriter = new MemberWriter(roomId); | ||
const syncWriter = new SyncWriter({ | ||
roomId, | ||
fragmentIdComparer, | ||
memberWriter, | ||
relationWriter | ||
}); | ||
return { storage, txn, fragmentIdComparer, gapWriter, syncWriter, timelineMock: new TimelineMock() }; | ||
} | ||
|
||
async function syncAndWrite(mocks, { previous, limit } = {}) { | ||
const {txn, timelineMock, syncWriter, fragmentIdComparer} = mocks; | ||
const syncResponse = timelineMock.sync(previous?.next_batch, limit); | ||
const {newLiveKey} = await syncWriter.writeSync(syncResponse, false, false, txn, logger); | ||
syncWriter.afterSync(newLiveKey); | ||
return { | ||
syncResponse, | ||
fragmentEntry: newLiveKey ? FragmentBoundaryEntry.start( | ||
await txn.timelineFragments.get(roomId, newLiveKey.fragmentId), | ||
fragmentIdComparer, | ||
) : null, | ||
}; | ||
} | ||
|
||
async function backfillAndWrite(mocks, fragmentEntry, limit) { | ||
const {txn, timelineMock, gapWriter} = mocks; | ||
const messageResponse = timelineMock.messages(fragmentEntry.token, undefined, fragmentEntry.direction.asApiString(), limit); | ||
await gapWriter.writeFragmentFill(fragmentEntry, messageResponse, txn, logger); | ||
} | ||
|
||
async function allFragmentEvents(mocks, fragmentId) { | ||
const {txn} = mocks; | ||
const entries = await txn.timelineEvents.eventsAfter(roomId, new EventKey(fragmentId, KeyLimits.minStorageKey)); | ||
return entries.map(e => e.event); | ||
} | ||
|
||
async function fetchFragment(mocks, fragmentId) { | ||
const {txn} = mocks; | ||
return txn.timelineFragments.get(roomId, fragmentId); | ||
} | ||
|
||
function assertFilledLink(assert, fragment1, fragment2) { | ||
assert.equal(fragment1.nextId, fragment2.id); | ||
assert.equal(fragment2.previousId, fragment1.id); | ||
assert.equal(fragment1.nextToken, null); | ||
assert.equal(fragment2.previousToken, null); | ||
} | ||
|
||
function assertGapLink(assert, fragment1, fragment2) { | ||
assert.equal(fragment1.nextId, fragment2.id); | ||
assert.equal(fragment2.previousId, fragment1.id); | ||
assert.notEqual(fragment2.previousToken, null); | ||
} | ||
|
||
return { | ||
"Backfilling after one sync": async assert => { | ||
const mocks = await setup(); | ||
const { timelineMock } = mocks; | ||
timelineMock.append(30); | ||
const {fragmentEntry} = await syncAndWrite(mocks); | ||
await backfillAndWrite(mocks, fragmentEntry, 10); | ||
const events = await allFragmentEvents(mocks, fragmentEntry.fragmentId); | ||
assert.deepEqual(events.map(e => e.event_id), eventIds(10, 30)); | ||
await mocks.txn.complete(); | ||
}, | ||
"Backfilling a fragment that is expected to close a gap, and does": async assert => { | ||
const mocks = await setup(); | ||
const { timelineMock } = mocks; | ||
timelineMock.append(10); | ||
const {syncResponse, fragmentEntry: firstFragmentEntry} = await syncAndWrite(mocks, { limit: 10 }); | ||
timelineMock.append(15); | ||
const {fragmentEntry: secondFragmentEntry} = await syncAndWrite(mocks, { previous: syncResponse, limit: 10 }); | ||
await backfillAndWrite(mocks, secondFragmentEntry, 10); | ||
|
||
const firstFragment = await fetchFragment(mocks, firstFragmentEntry.fragmentId); | ||
const secondFragment = await fetchFragment(mocks, secondFragmentEntry.fragmentId); | ||
assertFilledLink(assert, firstFragment, secondFragment) | ||
const firstEvents = await allFragmentEvents(mocks, firstFragmentEntry.fragmentId); | ||
assert.deepEqual(firstEvents.map(e => e.event_id), eventIds(0, 10)); | ||
const secondEvents = await allFragmentEvents(mocks, secondFragmentEntry.fragmentId); | ||
assert.deepEqual(secondEvents.map(e => e.event_id), eventIds(10, 25)); | ||
await mocks.txn.complete(); | ||
}, | ||
"Backfilling a fragment that is expected to close a gap, but doesn't yet": async assert => { | ||
const mocks = await setup(); | ||
const { timelineMock } = mocks; | ||
timelineMock.append(10); | ||
const {syncResponse, fragmentEntry: firstFragmentEntry} = await syncAndWrite(mocks, { limit: 10 }); | ||
timelineMock.append(20); | ||
const {fragmentEntry: secondFragmentEntry} = await syncAndWrite(mocks, { previous: syncResponse, limit: 10 }); | ||
await backfillAndWrite(mocks, secondFragmentEntry, 10); | ||
|
||
const firstFragment = await fetchFragment(mocks, firstFragmentEntry.fragmentId); | ||
const secondFragment = await fetchFragment(mocks, secondFragmentEntry.fragmentId); | ||
assertGapLink(assert, firstFragment, secondFragment) | ||
const firstEvents = await allFragmentEvents(mocks, firstFragmentEntry.fragmentId); | ||
assert.deepEqual(firstEvents.map(e => e.event_id), eventIds(0, 10)); | ||
const secondEvents = await allFragmentEvents(mocks, secondFragmentEntry.fragmentId); | ||
assert.deepEqual(secondEvents.map(e => e.event_id), eventIds(10, 30)); | ||
await mocks.txn.complete(); | ||
}, | ||
"Receiving a sync with the same events as the current fragment does not create infinite link": async assert => { | ||
const mocks = await setup(); | ||
const { txn, timelineMock } = mocks; | ||
timelineMock.append(10); | ||
const {syncResponse, fragmentEntry: fragmentEntry} = await syncAndWrite(mocks, { limit: 10 }); | ||
// Mess with the saved token to receive old events in backfill | ||
fragmentEntry.token = syncResponse.next_batch; | ||
txn.timelineFragments.update(fragmentEntry.fragment); | ||
await backfillAndWrite(mocks, fragmentEntry, 10); | ||
|
||
const fragment = await fetchFragment(mocks, fragmentEntry.fragmentId); | ||
assert.notEqual(fragment.nextId, fragment.id); | ||
assert.notEqual(fragment.previousId, fragment.id); | ||
await mocks.txn.complete(); | ||
}, | ||
"An event received by sync does not interrupt backfilling": async assert => { | ||
const mocks = await setup(); | ||
const { timelineMock } = mocks; | ||
timelineMock.append(10); | ||
const {syncResponse, fragmentEntry: firstFragmentEntry} = await syncAndWrite(mocks, { limit: 10 }); | ||
timelineMock.append(11); | ||
const {fragmentEntry: secondFragmentEntry} = await syncAndWrite(mocks, { previous: syncResponse, limit: 10 }); | ||
timelineMock.insertAfter(eventId(9), 5); | ||
await backfillAndWrite(mocks, secondFragmentEntry, 10); | ||
|
||
const firstEvents = await allFragmentEvents(mocks, firstFragmentEntry.fragmentId); | ||
assert.deepEqual(firstEvents.map(e => e.event_id), eventIds(0, 10)); | ||
const secondEvents = await allFragmentEvents(mocks, secondFragmentEntry.fragmentId); | ||
assert.deepEqual(secondEvents.map(e => e.event_id), [...eventIds(21,26), ...eventIds(10, 21)]); | ||
const firstFragment = await fetchFragment(mocks, firstFragmentEntry.fragmentId); | ||
const secondFragment = await fetchFragment(mocks, secondFragmentEntry.fragmentId); | ||
assertFilledLink(assert, firstFragment, secondFragment) | ||
await mocks.txn.complete(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.