# HG changeset patch # User drewp@bigasterisk.com # Date 2023-05-19 19:11:26 # Node ID 066f05ad79003f7795803814b74922a057647efc # Parent 7c03342eff15ebe6557206a3d2886a450b365a83 collector: even stronger types; repair test code (some are failing) diff --git a/light9/collector/collector.py b/light9/collector/collector.py --- a/light9/collector/collector.py +++ b/light9/collector/collector.py @@ -9,8 +9,8 @@ from light9.collector.device import reso from light9.collector.output import Output as OutputInstance from light9.collector.weblisteners import WebListeners from light9.namespaces import L9, RDF -from light9.newtypes import (ClientSessionType, ClientType, DeviceAttr, DeviceClass, DeviceUri, DmxIndex, DmxMessageIndex, OutputAttr, OutputRange, OutputUri, - OutputValue, UnixTime, typedValue) +from light9.newtypes import (ClientSessionType, ClientType, DeviceAttr, DeviceClass, DeviceSetting, DeviceUri, DmxIndex, DmxMessageIndex, OutputAttr, + OutputRange, OutputUri, OutputValue, UnixTime, VTUnion, typedValue) log = logging.getLogger('collector') @@ -69,10 +69,10 @@ class Collector: self.graph.addHandler(self.rebuildOutputMap) # client : (session, time, {(dev,devattr): latestValue}) - self.lastRequest: Dict[Tuple[ClientType, ClientSessionType], Tuple[UnixTime, Dict[Tuple[DeviceUri, DeviceAttr], float]]] = {} + self.lastRequest: Dict[Tuple[ClientType, ClientSessionType], Tuple[UnixTime, Dict[Tuple[DeviceUri, DeviceAttr], VTUnion]]] = {} # (dev, devAttr): value to use instead of 0 - self.stickyAttrs: Dict[Tuple[DeviceUri, DeviceAttr], float] = {} + self.stickyAttrs: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {} def rebuildOutputMap(self): self.outputMap = outputMap(self.graph, self.outputs) @@ -102,8 +102,8 @@ class Collector: del self.lastRequest[c] # todo: move to settings.py - def resolvedSettingsDict(self, settingsList: List[Tuple[DeviceUri, DeviceAttr, float]]) -> Dict[Tuple[DeviceUri, DeviceAttr], float]: - out: Dict[Tuple[DeviceUri, DeviceAttr], float] = {} + def resolvedSettingsDict(self, settingsList: List[DeviceSetting]) -> Dict[Tuple[DeviceUri, DeviceAttr], VTUnion]: + out: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {} for d, da, v in settingsList: if (d, da) in out: out[(d, da)] = resolve(d, da, [out[(d, da)], v]) @@ -118,12 +118,12 @@ class Collector: log.warn('collector.setAttrs from %s is running %.1fms after the request was made', client, requestLag * 1000) def _merge(self, lastRequests): - deviceAttrs: Dict[DeviceUri, Dict[DeviceAttr, float]] = {} # device: {deviceAttr: value} + deviceAttrs: Dict[DeviceUri, Dict[DeviceAttr, VTUnion]] = {} # device: {deviceAttr: value} for _, lastSettings in lastRequests: for (device, deviceAttr), value in lastSettings.items(): if (device, deviceAttr) in self.remapOut: start, end = self.remapOut[(device, deviceAttr)] - value = Literal(start + float(value) * (end - start)) + value = start + float(value) * (end - start) attrs = deviceAttrs.setdefault(device, {}) if deviceAttr in attrs: @@ -143,7 +143,7 @@ class Collector: return deviceAttrs - def setAttrs(self, client: ClientType, clientSession: ClientSessionType, settings: List[Tuple[DeviceUri, DeviceAttr, float]], sendTime: UnixTime): + def setAttrs(self, client: ClientType, clientSession: ClientSessionType, settings: List[DeviceSetting], sendTime: UnixTime): """ settings is a list of (device, attr, value). These attrs are device attrs. We resolve conflicting values, process them into @@ -178,7 +178,7 @@ class Collector: except Exception as e: log.error('failing toOutputAttrs on %s: %r', d, e) - pendingOut: Dict[OutputUri, Tuple[OutputInstance, bytearray]] = {} + pendingOut = cast(Dict[OutputUri, Tuple[OutputInstance, bytearray]], {}) for out in self.outputs: pendingOut[OutputUri(out.uri)] = (out, bytearray(512)) diff --git a/light9/collector/collector_test.py b/light9/collector/collector_test.py --- a/light9/collector/collector_test.py +++ b/light9/collector/collector_test.py @@ -1,11 +1,14 @@ import unittest import datetime, time from freezegun import freeze_time +from light9.collector.output import Output +from light9.collector.weblisteners import WebListeners from rdflib import Namespace from light9.namespaces import L9, DEV from light9.collector.collector import Collector, outputMap -from rdfdb.mock_syncedgraph import MockSyncedGraph +from light9.mock_syncedgraph import MockSyncedGraph +from light9.newtypes import ClientSessionType, ClientType, DeviceAttr, DeviceUri, HexColor, UnixTime UDMX = Namespace('http://light9.bigasterisk.com/output/udmx/') DMX0 = Namespace('http://light9.bigasterisk.com/output/dmx0/') @@ -35,10 +38,18 @@ THEATER = ''' ''' -t0 = 0 # time +t0 = UnixTime(0) +client1 = ClientType('client1') +client2 = ClientType('client2') +session1 = ClientSessionType('sess1') +session2 = ClientSessionType('sess2') +colorStrip = DeviceUri(DEV['colorStrip']) +color = DeviceAttr(L9['color']) +brightness = DeviceAttr(L9['brightness']) +inst1 = DeviceUri(DEV['inst1']) -class MockOutput(object): +class MockOutput(Output): def __init__(self, uri, connections): self.connections = connections @@ -46,54 +57,14 @@ class MockOutput(object): self.uri = uri self.numChannels = 4 - def allConnections(self): - return self.connections - def update(self, values): - self.updates.append(values) - - def flush(self): - self.updates.append('flush') + self.updates.append(list(values[:self.numChannels])) -@unittest.skip("outputMap got rewritten and mostly doesn't raise on these cases" - ) -class TestOutputMap(unittest.TestCase): - - def testWorking(self): - out0 = MockOutput(UDMX, [(0, DMX0['c1'])]) - m = outputMap( - MockSyncedGraph(PREFIX + ''' - dmx0:c1 :connectedTo dev:inst1Brightness . - dev:inst1 a :Device; :brightness dev:inst1Brightness . - '''), [out0]) - self.assertEqual({(DEV['inst1'], L9['brightness']): (out0, 0)}, m) +class MockWebListeners(WebListeners): - def testMissingOutput(self): - out0 = MockOutput(UDMX, [(0, DMX0['c1'])]) - self.assertRaises( - KeyError, outputMap, - MockSyncedGraph(PREFIX + ''' - dev:inst1 a :Device; :brightness dev:inst1Brightness . - '''), [out0]) - - def testMissingOutputConnection(self): - out0 = MockOutput(UDMX, [(0, DMX0['c1'])]) - self.assertRaises( - ValueError, outputMap, - MockSyncedGraph(PREFIX + ''' - dev:inst1 a :Device; :brightness dev:inst1Brightness . - '''), [out0]) - - def testMultipleOutputConnections(self): - out0 = MockOutput(UDMX, [(0, DMX0['c1'])]) - self.assertRaises( - ValueError, outputMap, - MockSyncedGraph(PREFIX + ''' - dmx0:c1 :connectedTo dev:inst1Brightness . - dmx0:c2 :connectedTo dev:inst1Brightness . - dev:inst1 a :Device; :brightness dev:inst1Brightness . - '''), [out0]) + def __init__(self): + "do not init" class TestCollector(unittest.TestCase): @@ -113,73 +84,58 @@ class TestCollector(unittest.TestCase): :level dev:inst1Brightness . ''') - self.dmx0 = MockOutput(DMX0[None], [(0, DMX0['c1'])]) - self.udmx = MockOutput(UDMX[None], [(0, UDMX['c1']), (1, UDMX['c2']), - (2, UDMX['c3']), (3, UDMX['c4'])]) + self.dmx0 = MockOutput(DMX0, [(0, DMX0['c1'])]) + self.udmx = MockOutput(UDMX, [(0, UDMX['c1']), (1, UDMX['c2']), (2, UDMX['c3']), (3, UDMX['c4'])]) def testRoutesColorOutput(self): - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) + + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#00ff00'))], t0) - c.setAttrs('client', 'sess1', - [(DEV['colorStrip'], L9['color'], '#00ff00')], t0) - - self.assertEqual([[215, 0, 255, 0], 'flush'], self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], 'flush'], self.dmx0.updates) + self.assertEqual([ + [215, 0, 255, 0], + ], self.udmx.updates) + self.assertEqual([ + [0, 0, 0, 0], + ], self.dmx0.updates) def testOutputMaxOfTwoClients(self): - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - c.setAttrs('client1', 'sess1', - [(DEV['colorStrip'], L9['color'], '#ff0000')], t0) - c.setAttrs('client2', 'sess1', - [(DEV['colorStrip'], L9['color'], '#333333')], t0) + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#ff0000'))], t0) + c.setAttrs(client2, session1, [(colorStrip, color, HexColor('#333333'))], t0) - self.assertEqual( - [[215, 255, 0, 0], 'flush', [215, 255, 51, 51], 'flush'], - self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], 'flush', [0, 0, 0, 0], 'flush'], - self.dmx0.updates) + self.assertEqual([[215, 255, 0, 0], [215, 255, 51, 51]], self.udmx.updates) + self.assertEqual([[0, 0, 0, 0], [0, 0, 0, 0]], self.dmx0.updates) def testClientOnSameOutputIsRememberedOverCalls(self): - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - c.setAttrs('client1', 'sess1', - [(DEV['colorStrip'], L9['color'], '#080000')], t0) - c.setAttrs('client2', 'sess1', - [(DEV['colorStrip'], L9['color'], '#060000')], t0) - c.setAttrs('client1', 'sess1', - [(DEV['colorStrip'], L9['color'], '#050000')], t0) + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#080000'))], t0) + c.setAttrs(client2, session1, [(colorStrip, color, HexColor('#060000'))], t0) + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#050000'))], t0) - self.assertEqual([[215, 8, 0, 0], 'flush', [215, 8, 0, 0], 'flush', - [215, 6, 0, 0], 'flush'], self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], 'flush', [0, 0, 0, 0], 'flush', - [0, 0, 0, 0], 'flush'], self.dmx0.updates) + self.assertEqual([[215, 8, 0, 0], [215, 8, 0, 0], [215, 6, 0, 0]], self.udmx.updates) + self.assertEqual([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], self.dmx0.updates) def testClientsOnDifferentOutputs(self): - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - c.setAttrs('client1', 'sess1', - [(DEV['colorStrip'], L9['color'], '#aa0000')], t0) - c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], - t0) + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#aa0000'))], t0) + c.setAttrs(client2, session1, [(inst1, brightness, .5)], t0) # ok that udmx is flushed twice- it can screen out its own duplicates - self.assertEqual([[215, 170, 0, 0], 'flush', [215, 170, 0, 0], 'flush'], - self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush'], - self.dmx0.updates) + self.assertEqual([[215, 170, 0, 0], [215, 170, 0, 0]], self.udmx.updates) + self.assertEqual([[0, 0, 0, 0], [127, 0, 0, 0]], self.dmx0.updates) def testNewSessionReplacesPreviousOutput(self): # ..as opposed to getting max'd with it - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)], - t0) - c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)], - t0) + c.setAttrs(client1, session1, [(inst1, brightness, .8)], t0) + c.setAttrs(client1, session2, [(inst1, brightness, .5)], t0) - self.assertEqual([[204, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush'], - self.dmx0.updates) + self.assertEqual([[204, 0, 0, 0], [127, 0, 0, 0]], self.dmx0.updates) def testNewSessionDropsPreviousSettingsOfOtherAttrs(self): c = Collector(MockSyncedGraph(PREFIX + THEATER + ''' @@ -195,60 +151,48 @@ class TestCollector(unittest.TestCase): :dmxUniverse dmx0:; :dmxBase 0; :level dev:inst1Brightness . '''), - outputs=[self.dmx0, self.udmx]) + outputs=[self.dmx0, self.udmx], + listeners=MockWebListeners()) - c.setAttrs('client1', 'sess1', - [(DEV['colorStrip'], L9['color'], '#ff0000')], t0) - c.setAttrs('client1', 'sess2', - [(DEV['colorStrip'], L9['color'], '#00ff00')], t0) + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#ff0000'))], t0) + c.setAttrs(client1, session2, [(colorStrip, color, HexColor('#00ff00'))], t0) - self.assertEqual([[215, 255, 0, 0], 'flush', [215, 0, 255, 0], 'flush'], - self.udmx.updates) + self.assertEqual([[215, 255, 0, 0], [215, 0, 255, 0]], self.udmx.updates) def testClientIsForgottenAfterAWhile(self): with freeze_time(datetime.datetime.now()) as ft: - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) - c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], - time.time()) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) + c.setAttrs(client1, session1, [(inst1, brightness, .5)], UnixTime(time.time())) ft.tick(delta=datetime.timedelta(seconds=1)) # this max's with cli1's value so we still see .5 - c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)], - time.time()) + c.setAttrs(client2, session1, [(inst1, brightness, .2)], UnixTime(time.time())) ft.tick(delta=datetime.timedelta(seconds=9.1)) # now cli1 is forgotten, so our value appears - c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)], - time.time()) - self.assertEqual([[127, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush', - [102, 0, 0, 0], 'flush'], self.dmx0.updates) + c.setAttrs(client2, session1, [(inst1, brightness, .4)], UnixTime(time.time())) + self.assertEqual([[127, 0, 0, 0], [127, 0, 0, 0], [102, 0, 0, 0]], self.dmx0.updates) def testClientUpdatesAreNotMerged(self): # second call to setAttrs forgets the first - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) - t0 = time.time() - c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], - t0) - c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)], - t0) - c.setAttrs('client1', 'sess1', - [(DEV['colorStrip'], L9['color'], '#00ff00')], t0) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) + t0 = UnixTime(time.time()) + c.setAttrs(client1, session1, [(inst1, brightness, .5)], t0) + c.setAttrs(client1, session1, [(inst1, brightness, 1)], t0) + c.setAttrs(client1, session1, [(colorStrip, color, HexColor('#00ff00'))], t0) - self.assertEqual([[215, 0, 0, 0], 'flush', [215, 0, 0, 0], 'flush', - [215, 0, 255, 0], 'flush'], self.udmx.updates) - self.assertEqual([[127, 0, 0, 0], 'flush', [255, 0, 0, 0], 'flush', - [0, 0, 0, 0], 'flush'], self.dmx0.updates) + self.assertEqual([[215, 0, 0, 0], [215, 0, 0, 0], [215, 0, 255, 0]], self.udmx.updates) + self.assertEqual([[127, 0, 0, 0], [255, 0, 0, 0], [0, 0, 0, 0]], self.dmx0.updates) def testRepeatedAttributesInOneRequestGetResolved(self): - c = Collector(self.config, outputs=[self.dmx0, self.udmx]) + c = Collector(self.config, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - c.setAttrs('client1', 'sess1', [ - (DEV['inst1'], L9['brightness'], .5), - (DEV['inst1'], L9['brightness'], .3), + c.setAttrs(client1, session1, [ + (inst1, brightness, .5), + (inst1, brightness, .3), ], t0) - self.assertEqual([[127, 0, 0, 0], 'flush'], self.dmx0.updates) + self.assertEqual([[127, 0, 0, 0]], self.dmx0.updates) - c.setAttrs('client1', 'sess1', [ - (DEV['inst1'], L9['brightness'], .3), - (DEV['inst1'], L9['brightness'], .5), + c.setAttrs(client1, session1, [ + (inst1, brightness, .3), + (inst1, brightness, .5), ], t0) - self.assertEqual([[127, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush'], - self.dmx0.updates) + self.assertEqual([[127, 0, 0, 0], [127, 0, 0, 0]], self.dmx0.updates) diff --git a/light9/collector/device.py b/light9/collector/device.py --- a/light9/collector/device.py +++ b/light9/collector/device.py @@ -5,7 +5,7 @@ from rdflib import Literal, URIRef from webcolors import hex_to_rgb, rgb_to_hex from colormath.color_objects import sRGBColor, CMYColor import colormath.color_conversions -from light9.newtypes import OutputAttr, OutputValue, DeviceUri, DeviceAttr +from light9.newtypes import VT, HexColor, OutputAttr, OutputValue, DeviceUri, DeviceAttr, VTUnion log = logging.getLogger('device') @@ -44,19 +44,16 @@ def _8bit(f): return clamp255(int(f * 255)) -def _maxColor(values: List[str]) -> str: +def _maxColor(values: List[HexColor]) -> HexColor: rgbs = [hex_to_rgb(v) for v in values] maxes = [max(component) for component in zip(*rgbs)] - return rgb_to_hex(tuple(maxes)) - - -VT = TypeVar('VT', float, int, str) + return cast(HexColor, rgb_to_hex(tuple(maxes))) def resolve( deviceType: DeviceUri, # should be DeviceClass? deviceAttr: DeviceAttr, - values: List[VT]) -> Any: # todo: return should be VT + values: List[VTUnion]) -> VTUnion: # todo: return should be VT """ return one value to use for this attr, given a set of them that have come in simultaneously. len(values) >= 1. @@ -66,7 +63,7 @@ def resolve( if len(values) == 1: return values[0] if deviceAttr == DeviceAttr(L9['color']): - return _maxColor(cast(List[str], values)) + return _maxColor(cast(List[HexColor], values)) # incomplete. how-to-resolve should be on the DeviceAttr defs in the graph. if deviceAttr in map(DeviceAttr, [L9['rx'], L9['ry'], L9['zoom'], L9['focus'], L9['iris']]): floatVals = [] @@ -79,7 +76,7 @@ def resolve( raise TypeError(repr(v)) # averaging with zeros? not so good - return Literal(sum(floatVals) / len(floatVals)) + return sum(floatVals) / len(floatVals) return max(values) diff --git a/light9/effect/settings_test.py b/light9/effect/settings_test.py --- a/light9/effect/settings_test.py +++ b/light9/effect/settings_test.py @@ -1,7 +1,7 @@ import unittest from rdflib import Literal from rdfdb.patch import Patch -from rdfdb.localsyncedgraph import LocalSyncedGraph +from light9.localsyncedgraph import LocalSyncedGraph from light9.namespaces import L9, DEV from light9.effect.settings import DeviceSettings @@ -87,7 +87,7 @@ class TestDeviceSettings(unittest.TestCa (L9['light1'], L9['attr2'], 0.3), (L9['light1'], L9['attr1'], 0.5), ] - self.assertItemsEqual(sets, DeviceSettings(self.graph, sets).asList()) + self.assertCountEqual(sets, DeviceSettings(self.graph, sets).asList()) def testDevices(self): s = DeviceSettings(self.graph, [ @@ -95,7 +95,7 @@ class TestDeviceSettings(unittest.TestCa (DEV['aura2'], L9['rx'], 0.1), ]) # aura1 is all defaults (zeros), so it doesn't get listed - self.assertItemsEqual([DEV['aura2']], s.devices()) + self.assertCountEqual([DEV['aura2']], s.devices()) def testAddStatements(self): s = DeviceSettings(self.graph, [ @@ -103,11 +103,12 @@ class TestDeviceSettings(unittest.TestCa ]) stmts = s.statements(L9['foo'], L9['ctx1'], L9['s_'], set()) self.maxDiff = None - self.assertItemsEqual([ - (L9['foo'], L9['setting'], L9['s_set4350023'], L9['ctx1']), - (L9['s_set4350023'], L9['device'], DEV['aura2'], L9['ctx1']), - (L9['s_set4350023'], L9['deviceAttr'], L9['rx'], L9['ctx1']), - (L9['s_set4350023'], L9['value'], Literal(0.1), L9['ctx1']), + setting = sorted(stmts)[-1][0] + self.assertCountEqual([ + (L9['foo'], L9['setting'], setting, L9['ctx1']), + (setting, L9['device'], DEV['aura2'], L9['ctx1']), + (setting, L9['deviceAttr'], L9['rx'], L9['ctx1']), + (setting, L9['value'], Literal(0.1), L9['ctx1']), ], stmts) def testDistanceTo(self): diff --git a/light9/newtypes.py b/light9/newtypes.py --- a/light9/newtypes.py +++ b/light9/newtypes.py @@ -1,4 +1,4 @@ -from typing import Tuple, NewType, Type, TypeVar, cast +from typing import Tuple, NewType, Type, TypeVar, Union, cast from rdflib import URIRef from rdflib.term import Node @@ -17,6 +17,13 @@ OutputValue = NewType('OutputValue', int Song = NewType('Song', URIRef) UnixTime = NewType('UnixTime', float) +VT = TypeVar('VT', float, int, str) # remove +HexColor = NewType('HexColor', str) +VTUnion = Union[float, int, HexColor] # rename to ValueType +DeviceSetting = Tuple[DeviceUri, DeviceAttr, + # currently, floats and hex color strings + VTUnion] + # Alternate output range for a device. Instead of outputting 0.0 to # 1.0, you can map that range into, say, 0.2 to 0.7 OutputRange = NewType('OutputRange', Tuple[float, float])