-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: cross-version sync with @comapeo/core@2.0.1
#941
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2e9e219
test: always connect peers more realistically
EvanHahn 88c7e90
test: cross-version sync with `@comapeo/core@2.0.1`
EvanHahn 101c33a
Merge branch 'main' into connect-peers-realistically-in-tests
EvanHahn 052960a
Merge branch 'connect-peers-realistically-in-tests' into sync-with-2.0.1
EvanHahn 3ff5856
Merge branch 'main' into sync-with-2.0.1
EvanHahn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { valueOf } from '@comapeo/schema' | ||
import { generate } from '@mapeo/mock-data' | ||
import assert from 'node:assert/strict' | ||
import test from 'node:test' | ||
import { | ||
connectPeers, | ||
createManager, | ||
createOldManagerOnVersion2_0_1, | ||
invite, | ||
waitForPeers, | ||
} from './utils.js' | ||
|
||
test('syncing @comapeo/core@2.0.1 with the current version', async (t) => { | ||
const oldManager = await createOldManagerOnVersion2_0_1('old') | ||
await oldManager.setDeviceInfo({ name: 'old', deviceType: 'mobile' }) | ||
|
||
const newManager = createManager('new', t) | ||
await newManager.setDeviceInfo({ name: 'new', deviceType: 'desktop' }) | ||
|
||
const managers = [oldManager, newManager] | ||
|
||
const disconnect = connectPeers(managers) | ||
t.after(disconnect) | ||
await waitForPeers(managers) | ||
|
||
const [oldManagerPeers, newManagerPeers] = await Promise.all( | ||
managers.map((manager) => manager.listLocalPeers()) | ||
) | ||
assert.equal(oldManagerPeers.length, 1, 'old manager sees 1 peer') | ||
assert.equal(newManagerPeers.length, 1, 'new manager sees 1 peer') | ||
assert( | ||
oldManagerPeers.some((p) => p.deviceId === newManager.deviceId), | ||
'old manager sees new manager' | ||
) | ||
assert( | ||
newManagerPeers.some((p) => p.deviceId === oldManager.deviceId), | ||
'new manager sees old manager' | ||
) | ||
|
||
const projectId = await oldManager.createProject({ name: 'foo bar' }) | ||
|
||
await invite({ | ||
projectId, | ||
invitor: oldManager, | ||
invitees: [newManager], | ||
}) | ||
|
||
const projects = await Promise.all( | ||
managers.map((manager) => manager.getProject(projectId)) | ||
) | ||
const [oldProject, newProject] = projects | ||
assert.equal( | ||
(await newProject.$getProjectSettings()).name, | ||
'foo bar', | ||
'new manager sees the project' | ||
) | ||
|
||
oldProject.$sync.start() | ||
newProject.$sync.start() | ||
|
||
const [oldObservation, newObservation] = await Promise.all( | ||
projects.map((project) => | ||
project.observation.create(valueOf(generate('observation')[0])) | ||
) | ||
) | ||
|
||
await Promise.all( | ||
projects.map((project) => project.$sync.waitForSync('full')) | ||
) | ||
|
||
assert( | ||
await oldProject.observation.getByDocId(newObservation.docId), | ||
'old project gets observation from new project' | ||
) | ||
assert( | ||
await newProject.observation.getByDocId(oldObservation.docId), | ||
'new project gets observation from old project' | ||
) | ||
}) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,16 @@ import sodium from 'sodium-universal' | |
import RAM from 'random-access-memory' | ||
import Fastify from 'fastify' | ||
import { arrayFrom } from 'iterpal' | ||
import assert from 'node:assert/strict' | ||
import * as path from 'node:path' | ||
import { fork } from 'node:child_process' | ||
import { createRequire } from 'node:module' | ||
import { fileURLToPath } from 'node:url' | ||
import * as v8 from 'node:v8' | ||
import { pEvent } from 'p-event' | ||
import { MapeoManager as MapeoManager_2_0_1 } from '@comapeo/core2.0.1' | ||
|
||
import { MapeoManager, roles } from '../src/index.js' | ||
import { kRPC } from '../src/mapeo-manager.js' | ||
import { generate } from '@mapeo/mock-data' | ||
import { valueOf } from '../src/utils.js' | ||
import { randomBytes, randomInt } from 'node:crypto' | ||
|
@@ -19,14 +20,26 @@ import fsPromises from 'node:fs/promises' | |
import { kSyncState } from '../src/sync/sync-api.js' | ||
import { readConfig } from '../src/config-import.js' | ||
|
||
/** @import { MemberApi } from '../src/member-api.js' */ | ||
|
||
const FAST_TESTS = !!process.env.FAST_TESTS | ||
const projectMigrationsFolder = new URL('../drizzle/project', import.meta.url) | ||
.pathname | ||
const clientMigrationsFolder = new URL('../drizzle/client', import.meta.url) | ||
.pathname | ||
|
||
/** | ||
* @param {readonly MapeoManager[]} managers | ||
* @internal | ||
* @typedef {Pick< | ||
* MapeoManager, | ||
* 'startLocalPeerDiscoveryServer' | | ||
* 'stopLocalPeerDiscoveryServer' | | ||
* 'connectLocalPeer' | ||
* >} ConnectableManager | ||
*/ | ||
|
||
/** | ||
* @param {ReadonlyArray<ConnectableManager>} managers | ||
* @returns {() => Promise<void>} | ||
*/ | ||
export function connectPeers(managers) { | ||
|
@@ -52,17 +65,40 @@ export function connectPeers(managers) { | |
} | ||
} | ||
|
||
/** | ||
* @internal | ||
* @typedef {WaitForPeersManager & { | ||
* getProject(projectId: string): PromiseLike<{ | ||
* $member: Pick<MemberApi, 'invite'> | ||
* }> | ||
* }} InvitorManager | ||
*/ | ||
|
||
/** | ||
* @internal | ||
* @typedef {WaitForPeersManager & { | ||
* deviceId: string | ||
* invite: { | ||
* on( | ||
* event: 'invite-received', | ||
* listener: (invite: { inviteId: string } | ||
* ) => unknown): void | ||
* accept(invite: unknown): PromiseLike<string> | ||
* reject(invite: unknown): unknown | ||
* } | ||
* }} InviteeManager | ||
*/ | ||
|
||
/** | ||
* Invite mapeo clients to a project | ||
* | ||
* @param {{ | ||
* invitor: MapeoManager, | ||
* projectId: string, | ||
* invitees: MapeoManager[], | ||
* roleId?: import('../src/roles.js').RoleIdAssignableToOthers, | ||
* roleName?: string | ||
* reject?: boolean | ||
* }} opts | ||
* @param {object} options | ||
* @param {string} options.projectId | ||
* @param {InvitorManager} options.invitor | ||
* @param {ReadonlyArray<InviteeManager>} options.invitees | ||
* @param {import('../src/roles.js').RoleIdAssignableToOthers} [options.roleId] | ||
* @param {string} [options.roleName] | ||
* @param {boolean} [options.reject] | ||
*/ | ||
export async function invite({ | ||
invitor, | ||
|
@@ -101,46 +137,68 @@ export async function invite({ | |
) | ||
} | ||
|
||
/** | ||
* A simple Promise-aware version of `Array.prototype.every`. | ||
* | ||
* Similar to the [p-every package](https://www.npmjs.com/package/p-every), | ||
* which I couldn't figure out how to import without type errors. | ||
* | ||
* @template T | ||
* @param {Iterable<T>} iterable | ||
* @param {(value: T) => boolean | PromiseLike<boolean>} predicate | ||
* @returns {Promise<boolean>} | ||
*/ | ||
async function pEvery(iterable, predicate) { | ||
const results = await Promise.all([...iterable].map(predicate)) | ||
return results.every(Boolean) | ||
} | ||
|
||
/** | ||
* @internal | ||
* @typedef {Pick<MapeoManager, 'deviceId' | 'listLocalPeers'> & { | ||
* on(event: 'local-peers', listener: () => unknown): void; | ||
* off(event: 'local-peers', listener: () => unknown): void; | ||
* }} WaitForPeersManager | ||
*/ | ||
|
||
/** | ||
* Waits for all manager instances to be connected to each other | ||
* | ||
* @param {readonly MapeoManager[]} managers | ||
* @param {ReadonlyArray<WaitForPeersManager>} managers | ||
* @param {{ waitForDeviceInfo?: boolean }} [opts] Optionally wait for device names to be set | ||
* @returns {Promise<void>} | ||
*/ | ||
export const waitForPeers = (managers, { waitForDeviceInfo = false } = {}) => | ||
new Promise((res) => { | ||
const deviceIds = new Set(managers.map((m) => m.deviceId)) | ||
|
||
const isDone = () => | ||
managers.every((manager) => { | ||
const unconnectedDeviceIds = new Set(deviceIds) | ||
unconnectedDeviceIds.delete(manager.deviceId) | ||
for (const peer of manager[kRPC].peers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had to do a small amount of work to stop calling a private method. |
||
if ( | ||
peer.status === 'connected' && | ||
(!waitForDeviceInfo || peer.name) | ||
) { | ||
unconnectedDeviceIds.delete(peer.deviceId) | ||
} | ||
export async function waitForPeers( | ||
managers, | ||
{ waitForDeviceInfo = false } = {} | ||
) { | ||
const deviceIds = new Set(managers.map((m) => m.deviceId)) | ||
|
||
/** @returns {Promise<boolean>} */ | ||
const isDone = async () => | ||
pEvery(managers, async (manager) => { | ||
const unconnectedDeviceIds = new Set(deviceIds) | ||
unconnectedDeviceIds.delete(manager.deviceId) | ||
for (const peer of await manager.listLocalPeers()) { | ||
if (peer.status === 'connected' && (!waitForDeviceInfo || peer.name)) { | ||
unconnectedDeviceIds.delete(peer.deviceId) | ||
} | ||
return unconnectedDeviceIds.size === 0 | ||
}) | ||
} | ||
return unconnectedDeviceIds.size === 0 | ||
}) | ||
|
||
if (isDone()) { | ||
res() | ||
return | ||
} | ||
if (await isDone()) return | ||
|
||
const onLocalPeers = () => { | ||
if (isDone()) { | ||
return new Promise((res) => { | ||
const onLocalPeers = async () => { | ||
if (await isDone()) { | ||
for (const manager of managers) manager.off('local-peers', onLocalPeers) | ||
res() | ||
} | ||
} | ||
|
||
for (const manager of managers) manager.on('local-peers', onLocalPeers) | ||
}) | ||
} | ||
|
||
/** | ||
* Create `count` manager instances. Each instance has a deterministic identity | ||
|
@@ -174,6 +232,7 @@ export async function createManagers( | |
* @param {string} seed | ||
* @param {import('node:test').TestContext} t | ||
* @param {Partial<ConstructorParameters<typeof MapeoManager>[0]>} [overrides] | ||
* @returns {MapeoManager} | ||
*/ | ||
export function createManager(seed, t, overrides = {}) { | ||
/** @type {string} */ let dbFolder | ||
|
@@ -208,6 +267,31 @@ export function createManager(seed, t, overrides = {}) { | |
...overrides, | ||
}) | ||
} | ||
/** | ||
* @param {string} seed | ||
* @param {Partial<ConstructorParameters<typeof MapeoManager_2_0_1>[0]>} [overrides] | ||
* @returns {Promise<MapeoManager_2_0_1>} | ||
*/ | ||
export async function createOldManagerOnVersion2_0_1(seed, overrides = {}) { | ||
const comapeoCorePreMigrationUrl = await import.meta.resolve?.( | ||
'@comapeo/core2.0.1' | ||
) | ||
assert(comapeoCorePreMigrationUrl, 'Could not resolve @comapeo/core2.0.1') | ||
|
||
return new MapeoManager_2_0_1({ | ||
rootKey: getRootKey(seed), | ||
clientMigrationsFolder: fileURLToPath( | ||
new URL('../drizzle/client', comapeoCorePreMigrationUrl) | ||
), | ||
projectMigrationsFolder: fileURLToPath( | ||
new URL('../drizzle/project', comapeoCorePreMigrationUrl) | ||
), | ||
dbFolder: ':memory:', | ||
coreStorage: () => new RAM(), | ||
fastify: Fastify(), | ||
...overrides, | ||
}) | ||
} | ||
|
||
/** | ||
* `ManagerCustodian` helps you test the creation of multiple managers accessing | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer needed.