// 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()
})