yjs/tests-lib/helper.js
2017-10-14 23:03:24 +02:00

279 lines
7.9 KiB
JavaScript

import _Y from '../../yjs/src/y.js'
import yTest from './test-connector.js'
import Chance from 'chance'
export let Y = _Y
export var database = { name: 'memory' }
export var connector = { name: 'test', url: 'http://localhost:1234' }
function getStateSet (y) {
let ss = {}
for (let [user, clock] of y.ss.state) {
ss[user] = clock
}
return ss
}
function getDeleteSet (y) {
var ds = {}
y.ds.iterate(this, null, null, function (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
var gc = n.gc
var dv = ds[user]
if (dv === void 0) {
dv = []
ds[user] = dv
}
dv.push([counter, len, gc])
})
return ds
}
export function attrsObject (dom) {
let keys = []
let yxml = dom.__yxml
for (let i = 0; i < dom.attributes.length; i++) {
keys.push(dom.attributes[i].name)
}
keys = yxml._domFilter(dom, keys)
let obj = {}
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
obj[key] = dom.getAttribute(key)
}
return obj
}
export function domToJson (dom) {
if (dom.nodeType === document.TEXT_NODE) {
return dom.textContent
} else if (dom.nodeType === document.ELEMENT_NODE) {
let attributes = attrsObject(dom)
let children = Array.from(dom.childNodes.values())
.filter(d => d.__yxml !== false)
.map(domToJson)
return {
name: dom.nodeName,
children: children,
attributes: attributes
}
} else {
throw new Error('Unsupported node type')
}
}
/*
* 1. reconnect and flush all
* 2. user 0 gc
* 3. get type content
* 4. disconnect & reconnect all (so gc is propagated)
* 5. compare os, ds, ss
*/
export async function compareUsers (t, users) {
await Promise.all(users.map(u => u.reconnect()))
if (users[0].connector.testRoom == null) {
await wait(100)
}
await flushAll(t, users)
await wait()
await flushAll(t, users)
var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON())
var userMapValues = users.map(u => u.get('map', Y.Map).toJSON())
var userXmlValues = users.map(u => u.get('xml', Y.Xml).getDom().toString())
// disconnect all except user 0
await Promise.all(users.slice(1).map(async u =>
u.disconnect()
))
if (users[0].connector.testRoom == null) {
await wait(100)
}
// reconnect all
await Promise.all(users.map(u => u.reconnect()))
if (users[0].connector.testRoom == null) {
await wait(100)
}
await users[0].connector.testRoom.flushAll(users)
await Promise.all(users.map(u =>
new Promise(function (resolve) {
u.connector.whenSynced(resolve)
})
))
var data = users.forEach(u => {
var data = {}
let ops = []
y.os.iterate(null, null, function (op) {
if (!op._deleted) {
ops.push({
left: op._left,
right: op._right,
deleted: op._deleted
})
}
})
data.os = {}
for (let i = 0; i < ops.length; i++) {
let op = ops[i]
op = Y.Struct[op.struct].encode(op)
delete op.origin
data.os[JSON.stringify(op.id)] = op
}
data.ds = getDeleteSet.apply(this)
data.ss = getStateSet.apply(this)
return data
})
for (var i = 0; i < data.length - 1; i++) {
await t.asyncGroup(async () => {
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
t.compare(userMapOneValues[i], userMapOneValues[i + 1], 'map types (propery "one")')
t.compare(userMapTwoValues[i], userMapTwoValues[i + 1], 'map types (propery "two")')
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
t.compare(data[i].os, data[i + 1].os, 'os')
t.compare(data[i].ds, data[i + 1].ds, 'ds')
t.compare(data[i].ss, data[i + 1].ss, 'ss')
}, `Compare user${i} with user${i + 1}`)
}
users.map(u => u.close())
}
export async function initArrays (t, opts) {
var result = {
users: []
}
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector)
for (let i = 0; i < opts.users; i++) {
let connOpts
if (i === 0) {
connOpts = Object.assign({ role: 'master' }, conn)
} else {
connOpts = Object.assign({ role: 'slave' }, conn)
}
let y = new Y({
connector: connOpts
})
result.users.push(y)
y.get('xml', Y.Xml).setDomFilter(function (d, attrs) {
if (d.nodeName === 'HIDDEN') {
return null
} else {
return attrs.filter(a => a !== 'hidden')
}
})
}
result.array0.delete(0, result.array0.length)
if (result.users[0].connector.testRoom != null) {
// flush for sync if test-connector
await result.users[0].connector.testRoom.flushAll(result.users)
}
await Promise.all(result.users.map(u => {
return new Promise(function (resolve) {
u.connector.whenSynced(resolve)
})
}))
await flushAll(t, result.users)
return result
}
export async function flushAll (t, users) {
// users = users.filter(u => u.connector.isSynced)
if (users.length === 0) {
return
}
await wait(0)
if (users[0].connector.testRoom != null) {
// use flushAll method specified in Test Connector
await users[0].connector.testRoom.flushAll(users)
} else {
// flush for any connector
await Promise.all(users.map(u => { return u.db.whenTransactionsFinished() }))
var flushCounter = users[0].get('flushHelper', Y.Map).get('0') || 0
flushCounter++
await Promise.all(users.map(async (u, i) => {
// wait for all users to set the flush counter to the same value
await new Promise(resolve => {
function observer () {
var allUsersReceivedUpdate = true
for (var i = 0; i < users.length; i++) {
if (u.get('flushHelper', Y.Map).get(i + '') !== flushCounter) {
allUsersReceivedUpdate = false
break
}
}
if (allUsersReceivedUpdate) {
resolve()
}
}
u.get('flushHelper', Y.Map).observe(observer)
u.get('flushHelper').set(i + '', flushCounter)
})
}))
}
}
export async function flushSome (t, users) {
if (users[0].connector.testRoom == null) {
// if not test-connector, wait for some time for operations to arrive
await wait(100)
}
}
export function wait (t) {
return new Promise(function (resolve) {
setTimeout(resolve, t != null ? t : 100)
})
}
export async function applyRandomTests (t, mods, iterations) {
const chance = new Chance(t.getSeed() * 1000000000)
var initInformation = await initArrays(t, { users: 5, chance: chance })
let { users } = initInformation
for (var i = 0; i < iterations; i++) {
if (chance.bool({likelihood: 10})) {
// 10% chance to disconnect/reconnect a user
// we make sure that the first users always is connected
let user = chance.pickone(users.slice(1))
if (user.connector.isSynced) {
if (users.filter(u => u.connector.isSynced).length > 1) {
// make sure that at least one user remains in the room
await user.disconnect()
if (users[0].connector.testRoom == null) {
await wait(100)
}
}
} else {
await user.reconnect()
if (users[0].connector.testRoom == null) {
await wait(100)
}
await new Promise(function (resolve) {
user.connector.whenSynced(resolve)
})
}
} else if (chance.bool({likelihood: 5})) {
// 20%*!prev chance to flush all & garbagecollect
// TODO: We do not gc all users as this does not work yet
// await garbageCollectUsers(t, users)
await flushAll(t, users)
await users[0].db.emptyGarbageCollector()
await flushAll(t, users)
} else if (chance.bool({likelihood: 10})) {
// 20%*!prev chance to flush some operations
await flushSome(t, users)
}
let user = chance.pickone(users)
var test = chance.pickone(mods)
test(t, user, chance)
}
await compareUsers(t, users)
return initInformation
}