Skip to content
Snippets Groups Projects
SpecHelper.js 11.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • Kevin Jahns's avatar
    Kevin Jahns committed
    /* eslint-env browser, jasmine */
    
    /*
      This is just a compilation of functions that help to test this library!
    */
    
    // When testing, you store everything on the global object. We call it g
    
    var Y = require('./y.js')
    
    require('../../y-memory/src/Memory.js')(Y)
    require('../../y-array/src/Array.js')(Y)
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    require('../../y-map/src/Map.js')(Y)
    
    require('../../y-indexeddb/src/IndexedDB.js')(Y)
    
    if (typeof window === 'undefined') {
      require('../../y-leveldb/src/LevelDB.js')(Y)
    }
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    module.exports = Y
    
    var g
    if (typeof global !== 'undefined') {
      g = global
    } else if (typeof window !== 'undefined') {
      g = window
    } else {
      throw new Error('No global object?')
    }
    g.g = g
    
    
    // Helper methods for the random number generator
    Math.seedrandom = require('seedrandom')
    
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    g.generateRandomSeed = function generateRandomSeed () {
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      if (typeof window !== 'undefined' && window.location.hash.length > 1) {
    
        seed = window.location.hash.slice(1) // first character is the hash!
        console.warn('Using random seed that was specified in the url!')
      } else {
        seed = JSON.stringify(Math.random())
      }
      console.info('Using random seed: ' + seed)
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      g.setRandomSeed(seed)
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    g.setRandomSeed = function setRandomSeed (seed) {
    
      Math.seedrandom.currentSeed = seed
      Math.seedrandom(Math.seedrandom.currentSeed, { global: true })
    }
    
    g.generateRandomSeed()
    
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    g.YConcurrency_TestingMode = true
    
    
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    
    g.describeManyTimes = function describeManyTimes (times, name, f) {
      for (var i = 0; i < times; i++) {
        describe(name, f)
      }
    }
    
    /*
      Wait for a specified amount of time (in ms). defaults to 5ms
    */
    function wait (t) {
      if (t == null) {
    
        t = 0
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      }
      return new Promise(function (resolve) {
        setTimeout(function () {
          resolve()
    
        }, t)
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      })
    }
    g.wait = wait
    
    
    g.databases = ['memory']
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    if (typeof window !== 'undefined') {
    
      g.databases.push('indexeddb')
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    }
    /*
      returns a random element of o.
      works on Object, and Array
    */
    function getRandom (o) {
      if (o instanceof Array) {
        return o[Math.floor(Math.random() * o.length)]
      } else if (o.constructor === Object) {
    
        return o[getRandom(Object.keys(o))]
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      }
    }
    g.getRandom = getRandom
    
    function getRandomNumber (n) {
      if (n == null) {
        n = 9999
      }
      return Math.floor(Math.random() * n)
    }
    g.getRandomNumber = getRandomNumber
    
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    function getRandomString () {
    
      var chars = 'abcdefghijklmnopqrstuvwxyzäüöABCDEFGHIJKLMNOPQRSTUVWXYZÄÜÖ'
    
      var char = chars[getRandomNumber(chars.length)] // ü\n\n\n\n\n\n\n'
      var length = getRandomNumber(7)
      var string = ''
      for (var i = 0; i < length; i++) {
        string += char
      }
      return string
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    }
    g.getRandomString = getRandomString
    
    
    function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) {
    
      g.generateRandomSeed() // create a new seed, so we can re-create the behavior
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
        var r = Math.random()
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          // 10% chance of toggling concurrent user interactions.
          // There will be an artificial delay until ops can be executed by the type,
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          // therefore, operations of the database will be (pre)transformed until user operations arrive
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          yield (function simulateConcurrentUserInteractions (type) {
    
    Kevin Jahns's avatar
    Kevin Jahns committed
            if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
              // usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead 
              type = type.y
            }
    
    Kevin Jahns's avatar
    Kevin Jahns committed
            if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
              type.eventHandler.awaiting = 1
              type.eventHandler._debuggingAwaiting = true
            } else {
    
              // fixAwaitingInType will handle _debuggingAwaiting
    
    Kevin Jahns's avatar
    Kevin Jahns committed
              return fixAwaitingInType(type)
            }
          })(getRandom(objects))
        } else if (r >= 0.5) {
          // 40% chance to flush
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0)
    
        } else if (noReconnect || r >= 0.05) {
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          // 45% chance to create operation
    
          var done = getRandom(transactions)(getRandom(objects))
          if (done != null) {
            yield done
          } else {
            yield wait()
          }
    
          yield Y.utils.globalRoom.whenTransactionsFinished()
    
    Kevin Jahns's avatar
    Kevin Jahns committed
        } else {
          // 5% chance to disconnect/reconnect
          var u = getRandom(users)
    
          yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          if (u.connector.isDisconnected()) {
            yield u.reconnect()
          } else {
            yield u.disconnect()
          }
    
          yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
        // usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead 
        type = type.y
      }
    
      return new Promise(function (resolve) {
        type.os.whenTransactionsFinished().then(function () {
          // _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
          type.os.requestTransaction(function * () {
            if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
              type.eventHandler._debuggingAwaiting = false
    
    Kevin Jahns's avatar
    Kevin Jahns committed
              yield* type.eventHandler.awaitOps(this, function * () { /* mock function */ })
    
            }
            wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
          })
        })
      })
    }
    g.fixAwaitingInType = fixAwaitingInType
    
    
    g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
      yield* applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
      yield Y.utils.globalRoom.flushAll()
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
      yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
      yield Y.utils.globalRoom.flushAll()
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      for (var u in users) {
    
        yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
        yield users[u].reconnect()
    
        yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      }
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
      yield Y.utils.globalRoom.flushAll()
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      yield g.garbageCollectAllUsers(users)
    })
    
    g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
      yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
    
      yield Y.utils.globalRoom.flushAll()
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      for (var u in users) {
        // TODO: here, we enforce that two users never sync at the same time with u[0]
        //       enforce that in the connector itself!
        yield users[u].reconnect()
      }
    
      yield Y.utils.globalRoom.flushAll()
    
      yield Promise.all(objects.map(fixAwaitingInType))
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      yield g.garbageCollectAllUsers(users)
    })
    
    g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      yield Y.utils.globalRoom.flushAll()
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      for (var i in users) {
    
        yield users[i].db.emptyGarbageCollector()
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      }
    })
    
    g.compareAllUsers = async(function * compareAllUsers (users) {
      var s1, s2 // state sets
      var ds1, ds2 // delete sets
      var allDels1, allDels2 // all deletions
      var db1 = [] // operation store of user1
    
      // t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
      function * t1 () {
        s1 = yield* this.getStateSet()
        ds1 = yield* this.getDeleteSet()
        allDels1 = []
        yield* this.ds.iterate(this, null, null, function * (d) {
          allDels1.push(d)
        })
      }
      function * t2 () {
        s2 = yield* this.getStateSet()
        ds2 = yield* this.getDeleteSet()
        allDels2 = []
        yield* this.ds.iterate(this, null, null, function * (d) {
          allDels2.push(d)
        })
      }
    
      yield Y.utils.globalRoom.flushAll()
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      yield g.garbageCollectAllUsers(users)
    
      yield Y.utils.globalRoom.flushAll()
      var buffer = Y.utils.globalRoom.buffers
      for (var name in buffer) {
        if (buffer[name].length > 0) {
    
          // not all ops were transmitted..
          debugger // eslint-disable-line
    
    Kevin Jahns's avatar
    Kevin Jahns committed
    
      for (var uid = 0; uid < users.length; uid++) {
        var u = users[uid]
        u.db.requestTransaction(function * () {
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          var sv = yield* this.getStateVector()
          for (var s of sv) {
            yield* this.updateState(s.user)
          }
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          // compare deleted ops against deleteStore
          yield* this.os.iterate(this, null, null, function * (o) {
            if (o.deleted === true) {
              expect(yield* this.isDeleted(o.id)).toBeTruthy()
            }
          })
          // compare deleteStore against deleted ops
          var ds = []
          yield* this.ds.iterate(this, null, null, function * (d) {
            ds.push(d)
          })
          for (var j in ds) {
            var d = ds[j]
            for (var i = 0; i < d.len; i++) {
    
              var o = yield* this.getInsertion([d.id[0], d.id[1] + i])
    
    Kevin Jahns's avatar
    Kevin Jahns committed
              // gc'd or deleted
              if (d.gc) {
                expect(o).toBeFalsy()
              } else {
                expect(o.deleted).toBeTruthy()
              }
            }
          }
        })
        // compare allDels tree
        if (s1 == null) {
          u.db.requestTransaction(function * () {
            yield* t1.call(this)
            yield* this.os.iterate(this, null, null, function * (o) {
              o = Y.utils.copyObject(o)
              delete o.origin
    
              delete o.originOf
    
    Kevin Jahns's avatar
    Kevin Jahns committed
              db1.push(o)
            })
          })
        } else {
          u.db.requestTransaction(function * () {
            yield* t2.call(this)
    
            var db2 = []
    
    Kevin Jahns's avatar
    Kevin Jahns committed
            yield* this.os.iterate(this, null, null, function * (o) {
              o = Y.utils.copyObject(o)
              delete o.origin
    
              delete o.originOf
    
              db2.push(o)
            })
            expect(s1).toEqual(s2)
            expect(allDels1).toEqual(allDels2) // inner structure
            expect(ds1).toEqual(ds2) // exported structure
            db2.forEach((o, i) => {
              expect(db1[i]).toEqual(o)
    
    Kevin Jahns's avatar
    Kevin Jahns committed
            })
          })
        }
    
        yield u.db.whenTransactionsFinished()
    
    g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      if (Y.utils.globalRoom.users[0] != null) {
    
        yield Y.utils.globalRoom.flushAll()
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      }
      // destroy old users
      for (var u in Y.utils.globalRoom.users) {
        Y.utils.globalRoom.users[u].y.destroy()
      }
      self.users = null
    
      var promises = []
      for (var i = 0; i < numberOfUsers; i++) {
        promises.push(Y({
          db: {
            name: database,
            namespace: 'User ' + i,
            cleanStart: true,
            gcTimeout: -1
          },
          connector: {
            name: 'Test',
            debug: false
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          },
          share: {
    
            root: initType || 'Map'
    
    Kevin Jahns's avatar
    Kevin Jahns committed
          }
        }))
      }
      self.users = yield Promise.all(promises)
    
      self.types = self.users.map(function (u) { return u.share.root })
    
    Kevin Jahns's avatar
    Kevin Jahns committed
      return self.users
    })
    
    /*
      Until async/await arrives in js, we use this function to wait for promises
      by yielding them.
    */
    function async (makeGenerator) {
      return function (arg) {
        var generator = makeGenerator.apply(this, arguments)
    
        function handle (result) {
          if (result.done) return Promise.resolve(result.value)
    
          return Promise.resolve(result.value).then(function (res) {
            return handle(generator.next(res))
          }, function (err) {
            return handle(generator.throw(err))
          })
        }
        try {
          return handle(generator.next())
        } catch (ex) {
          generator.throw(ex)
          // return Promise.reject(ex)
        }
      }
    }
    g.async = async
    
    function logUsers (self) {
      if (self.constructor === Array) {
        self = {users: self}
      }
      self.users[0].db.logTable()
      self.users[1].db.logTable()
      self.users[2].db.logTable()
    }
    
    g.logUsers = logUsers