OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / webui / test / rig-v3.test.js

130 lines · javascript ref:
// The rig against a proto-3 unit: the server referees its own browser sessions
// (they share ONE socket, and the unit sees one socket as one holder), and the
// unit referees everyone else.
import test from 'brittle'
import EventEmitter from 'bare-events'
import { Rig } from '../lib/rig.js'
import { FakeUnitV3, waitFor, sleep } from './helpers.js'

class FakeDiscovery extends EventEmitter {
  constructor () { super(); this.available = true; this.reason = '' }
  start () {}
  stop () {}
  announce (fake) {
    this.emit('up', { name: fake.name, host: fake.name + '.local', addr: '127.0.0.1', port: fake.port, proto: 3 })
  }
}

async function setup ({ values, stored, viewers = 0 } = {}) {
  const fake = await new FakeUnitV3('u3').listen()
  if (values) fake.values = values
  const discovery = new FakeDiscovery()
  const data = stored ? { units: { u3: { enabled: true, fadeMs: 0, slots: stored } } } : {}
  const rig = new Rig({ store: { load: () => data, save: () => {} }, discovery, readbackMs: 50, unitOpts: { backoffStartMs: 100 } })
  rig.start()
  rig.setViewers(viewers)
  discovery.announce(fake)
  rig.updateUnit('u3', { enabled: true, fadeMs: 0 })
  rig.setConnected(true)
  await waitFor(() => rig.snapshot().units[0].slots !== null)
  return { fake, rig, teardown: async () => { rig.stop(); await fake.close() } }
}

const unit = (rig) => rig.snapshot().units[0]

test('connect adopts the lamp, pushes nothing, asks state once', async (t) => {
  // A stored look AND a page watching: proto 2 would push the look and poll.
  const white = { r: 1, g: 1, b: 1, level: 1 }
  const { fake, rig, teardown } = await setup({
    values: [0, 0, 30000, 0, 0, 0, 0, 0, 0, 0, 0, 0], stored: [white, white, white, white], viewers: 1
  })
  await waitFor(() => unit(rig).slots[0].r === 1 && unit(rig).slots[0].g === 0)
  await sleep(300)   // several readback periods
  t.is(fake.received((l) => l.startsWith('pwm')).length, 0, 'no whole-unit push')
  t.is(fake.received((l) => l === 'state').length, 1, 'state asked once, then broadcasts')
  t.is(unit(rig).slots[0].r, 1, 'slot 1 adopted as red')
  await teardown()
})

test('sessions are refereed locally; the unit referees other controllers', async (t) => {
  const { fake, rig, teardown } = await setup()
  const a = await rig.grab('u3', 0, 'sessA', 'alice')
  t.ok(a.granted, 'alice grabs slot 1')
  const b = await rig.grab('u3', 0, 'sessB', 'bob')
  t.alike(b, { busy: 'alice' }, 'bob is refused by the server itself')
  t.is(fake.received((l) => l.startsWith('grant 0')).length, 1, 'only one grant reached the unit')
  t.ok((await rig.grab('u3', 1, 'sessB', 'bob')).granted, 'bob can take another slot')

  const ext = await fake.externalGrab(2, 'blender')
  t.alike(await rig.grab('u3', 2, 'sessA', 'alice'), { busy: 'blender' }, 'the unit refuses a slot Blender holds')
  t.ok(await waitFor(() => unit(rig).holds[2] && unit(rig).holds[2].holder === 'blender'), 'holder shown')
  t.is(unit(rig).holds[2].session, null, 'not one of ours')
  t.is(unit(rig).holds[0].session, 'sessA')
  ext.destroy()
  await teardown()
})

test('only the holding session can set a slot; frames reach the unit', async (t) => {
  const { fake, rig, teardown } = await setup()
  await rig.grab('u3', 1, 'sessA', 'alice')
  t.exception(() => rig.setSlot('u3', 1, { r: 1, g: 0, b: 0, level: 1 }, 'sessB'), /not holding/)
  rig.setSlot('u3', 1, { r: 1, g: 0, b: 0, level: 1 }, 'sessA')
  t.ok(await waitFor(() => fake.values[5] === 30000), 'red at the 30000 cap on slot 2')
  t.ok(fake.received((l) => l.startsWith('slot 1 5 ')).length >= 1)
  await teardown()
})

test('release: the final value is kept and becomes the slot\'s stored look', async (t) => {
  const { rig, teardown } = await setup()
  await rig.grab('u3', 3, 'sessA', 'alice')
  rig.setSlot('u3', 3, { r: 0, g: 0, b: 1, level: 0.5 }, 'sessA')
  rig.release('u3', 3, 'sessA')
  t.ok(await waitFor(() => unit(rig).holds[3] === null), 'freed')
  const s = unit(rig).slots[3]
  t.ok(Math.abs(s.b - 1) < 1e-6 && Math.abs(s.level - 0.5) < 0.001, 'stored look = what the lamp ended on')
  await teardown()
})

test('another controller\'s release updates our view of the slot', async (t) => {
  const { rig, fake, teardown } = await setup()
  const ext = await fake.externalGrab(0, 'blender')
  ext.write('slot 0 5 1 0 65535 0 0\n')
  ext.write('release 0\n')
  t.ok(await waitFor(() => unit(rig).slots[0].g === 1 && unit(rig).holds[0] === null), 'slot 1 now green, free')
  ext.destroy()
  await teardown()
})

test('a reconnect clears our grants', async (t) => {
  const { fake, rig, teardown } = await setup()
  await rig.grab('u3', 0, 'sessA', 'alice')
  for (const c of fake.conns.values()) c.destroy()
  t.ok(await waitFor(() => unit(rig).holds[0] === null), 'grant forgotten')
  t.exception(() => rig.setSlot('u3', 0, { r: 1, g: 1, b: 1, level: 1 }, 'sessA'), /not holding/)
  await teardown()
})

test('recalling a look takes all four slots, fades, and lets go', async (t) => {
  const { fake, rig, teardown } = await setup()
  rig.updateUnit('u3', { slots: [{ r: 0, g: 1, b: 0, level: 1 }, null, null, null] })
  rig.saveLook('green')
  await rig.recallLook('green', 1500)
  t.ok(await waitFor(() => fake.received((l) => l === 'release all').length === 1), 'released')
  t.is(fake.received((l) => l.startsWith('grant all')).length, 1)
  t.ok(fake.received((l) => /^slot 0 5 \d+ 0 65535 0 1500$/.test(l)).length === 1, 'slot 1 to green over 1.5 s')
  t.is(fake.received((l) => l.startsWith('pwm')).length, 0, 'no whole-unit pwm')
  await teardown()
})

test('recall skips a unit someone else is holding', async (t) => {
  const { fake, rig, teardown } = await setup()
  rig.updateUnit('u3', { slots: [{ r: 1, g: 1, b: 1, level: 1 }, null, null, null] })
  rig.saveLook('white')
  const ext = await fake.externalGrab(2, 'blender')
  const r = await rig.recallLook('white', 0)
  t.alike(r.skipped, [{ unit: 'u3', holder: 'blender' }])
  t.is(fake.received((l) => l.startsWith('slot ')).length, 0)
  ext.destroy()
  await teardown()
})