summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-09-26 19:08:01 -0400
committerBen Connors <benconnors@outlook.com>2019-09-26 19:08:01 -0400
commitcefc580a2f38f14c0245c9d6a5acbaa67feaf8d4 (patch)
treea055ef1931c4f8da009aa9f61f4640a37b25e4fe
parent9bd3390071be3db8c366d44e161e828c8263179b (diff)
Various fixes; start implementing tests
-rw-r--r--blc2/functions/audio.py23
-rw-r--r--blc2/functions/function.py5
-rw-r--r--blc2/functions/scene.py26
-rw-r--r--blc2/workspace.py6
-rw-r--r--examples/workspace.xml4
-rw-r--r--tests/conftest.py18
-rw-r--r--tests/silence.m4abin0 -> 2130 bytes
-rw-r--r--tests/test_functions_audio.py36
-rw-r--r--tests/test_functions_scene.py76
-rw-r--r--tests/test_topology.py55
10 files changed, 232 insertions, 17 deletions
diff --git a/blc2/functions/audio.py b/blc2/functions/audio.py
index 0d59856..42cd3f6 100644
--- a/blc2/functions/audio.py
+++ b/blc2/functions/audio.py
@@ -66,6 +66,7 @@ class Audio(Function):
raise ValueError("Fades must be nonnegative")
if v != self._fade_out:
+ self._duration = max(0, self._duration+self._fade_out-v)
self._fade_out = v
self.w.function_changed(self)
@@ -75,7 +76,7 @@ class Audio(Function):
@property
def audio_scope(self):
- return self._audio_scope.union()
+ return self._audio_scope
@property
def actual_duration(self):
@@ -85,16 +86,16 @@ class Audio(Function):
def duration(self):
return self._duration
- def _set_duration(self, value):
+ def _set_duration(self, value, update=True):
"""Set the duration.
This is called by Workspace when the duration for the file has been rechecked; do
not call this elsewhere, duration is intended to be immutable from outside the
library.
"""
- if value != self._duration:
- self._duration = max(value-self._fade_out, 0)
- self._actual_duration = value
+ self._duration = max(value-self._fade_out, 0)
+ self._actual_duration = value
+ if update:
self.w.function_changed(self)
def get_data(self):
@@ -104,8 +105,10 @@ class Audio(Function):
return None
def render(self, t, data = None):
+ if t > self._actual_duration:
+ return (), (), None
return ((),
- ((self._audio_id, self._filename, 0, self.fade_in, max(0, self.duration-self.fade_out), self.fade_out),),
+ ((self._audio_id, self._filename, 0, self.fade_in, self._duration, self.fade_out),),
None)
@property
@@ -117,11 +120,11 @@ class Audio(Function):
def filename(self, value: str):
"""Set the current audio filename, updating the duration."""
if value is not None:
- self.duration = self.w.get_audio_length(value)
- self.audio_scope = frozenset(((value,),))
+ self._set_duration(self.w.get_audio_length(value), update=False)
+ self._audio_scope = frozenset((value,))
else:
- self.duration = 0
- self.audio_scope = frozenset()
+ self._set_duration(0, update=False)
+ self._audio_scope = frozenset()
self._filename = value
self._audio_id = hash((self._audio_id, self._filename))
diff --git a/blc2/functions/function.py b/blc2/functions/function.py
index d210936..79d3b2b 100644
--- a/blc2/functions/function.py
+++ b/blc2/functions/function.py
@@ -21,7 +21,10 @@ class Function(XMLSerializable, metaclass=ABCMeta):
Any change to the scope, audio scope, or actual duration of the function must be
reported using ``Workspace.function_changed`` with the new values. This should also be
- extended to any change in e.g. values for lighting and audio primitives.
+ extended to any change in e.g. values for lighting and audio primitives.
+
+ It is an error to change Function.type or Function.fade_out_mode in user code. These
+ values are public for informational purposes.
"""
type: FUNCTION
fade_out_mode: EXTERNAL
diff --git a/blc2/functions/scene.py b/blc2/functions/scene.py
index ddda1c5..c786287 100644
--- a/blc2/functions/scene.py
+++ b/blc2/functions/scene.py
@@ -26,19 +26,34 @@ class Scene(Function):
same fashion. Alternatively, values can be updated in bulk using ``Scene.update`` or
set in bulk with ``Scene.set``.
"""
- audio_scope = ()
- scope = frozenset()
- values = frozenset()
type = SCENE
- def __init__(self, w, id_ = None, name = None, values: Mapping[Fixture.Channel, int] = None):
+ def __init__(self, w, id_ = None, name = None, values: Mapping[Fixture.Channel, int] = ()):
super().__init__(w=w, id_=id_, name=name)
self._values = {}
self._render = ()
+ self._scope = ()
self._update(values, changed=False)
@property
+ def scope(self):
+ return self._scope
+
+ @property
+ def values(self):
+ """Return the scene's values."""
+ return self._values.copy()
+
+ @values.setter
+ def values(self, v):
+ self.set(v)
+
+ @property
+ def audio_scope(self):
+ return frozenset()
+
+ @property
def fade_in(self):
return 0
@@ -56,8 +71,7 @@ class Scene(Function):
def _update_render(self, changed=True):
self._render = tuple(self._values.items())
- self.scope = frozenset(self._values.keys())
- self.values = self._render
+ self._scope = frozenset(self._values.keys())
if changed:
self.w.function_changed(self)
diff --git a/blc2/workspace.py b/blc2/workspace.py
index 51b35af..55757d3 100644
--- a/blc2/workspace.py
+++ b/blc2/workspace.py
@@ -89,6 +89,7 @@ class Workspace(XMLSerializable):
"""
if f.id in self.fixtures:
raise ValueError("A fixture with that ID already exists")
+ self._last_fixture_id = max(self._last_fixture_id, f.id)
self.fixtures[f.id] = f
def register_function(self, f: Function):
@@ -98,6 +99,7 @@ class Workspace(XMLSerializable):
"""
if f.id in self.functions:
raise ValueError("A function with that ID already exists")
+ self._last_function_id = max(self._last_function_id, f.id)
self.functions[f.id] = f
@property
@@ -183,6 +185,8 @@ class Workspace(XMLSerializable):
:param f: the changed function
"""
+ if f.id not in self._change_callbacks:
+ return
for callback in self._change_callbacks[f.id]:
callback(f)
@@ -191,6 +195,8 @@ class Workspace(XMLSerializable):
This also handles removing the function from the Workspace.
"""
+ if f.id not in self._change_callbacks:
+ return
for callback in self._delete_callbacks[f.id]:
callback(f)
diff --git a/examples/workspace.xml b/examples/workspace.xml
index 8be8bbe..a2be9e0 100644
--- a/examples/workspace.xml
+++ b/examples/workspace.xml
@@ -34,5 +34,9 @@
<function type="Audio" id="1" name="Intro" fade-in="0" fade-out="0">
<filename>test.wav</filename>
</function>
+
+ <function type="Chaser" id="2" name="Chaser 1">
+ <step id="0" name="Step 1" fade-in="0" fade-out="0" duration-mode="Manual" duration="123" function="0"/>
+ </function>
</functions>
</workspace>
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..71bd3ac
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,18 @@
+import datetime as dt
+
+import pytest
+
+from blc2.workspace import Workspace
+from blc2.topology import Fixture
+
+@pytest.fixture
+def ws():
+ """Return a workspace."""
+ return Workspace("", "" ,"", dt.datetime.now())
+
+@pytest.fixture
+def aws():
+ """Return a more advanced workspace with 4 channels on 1 fixture."""
+ w = Workspace("", "", "", dt.datetime.now())
+ Fixture(w, id_=0, channel_count=4)
+ return w
diff --git a/tests/silence.m4a b/tests/silence.m4a
new file mode 100644
index 0000000..0775df6
--- /dev/null
+++ b/tests/silence.m4a
Binary files differ
diff --git a/tests/test_functions_audio.py b/tests/test_functions_audio.py
new file mode 100644
index 0000000..de6aa2a
--- /dev/null
+++ b/tests/test_functions_audio.py
@@ -0,0 +1,36 @@
+import os
+
+from blc2.functions.audio import Audio
+
+def test_audio(aws):
+ a = Audio(aws)
+
+ a.fade_out = 1000
+ a.fade_in = 1000
+ a.filename = "nonexistant"
+
+ assert a.audio_scope == {"nonexistant"}
+ assert not a.scope
+ assert a.duration == 0
+ assert a.actual_duration == 0
+
+ a.filename = "tests/silence.m4a"
+ assert a.audio_scope == {"tests/silence.m4a"}
+ assert a.duration == 2024
+ assert a.actual_duration == 3024
+
+ a.fade_out = 500
+ assert a.fade_out == 500
+ assert a.duration == 2524
+ assert a.actual_duration == 3024
+
+ lc, ac, _ = a.render(0)
+ assert not lc
+ assert len(ac) == 1
+ assert ac[0][1:] == ("tests/silence.m4a", 0, 1000, 2524, 500)
+
+ _, ac2, _ = a.render(3000)
+ assert ac2 == ac
+
+ _, ac, _ = a.render(5000)
+ assert not ac
diff --git a/tests/test_functions_scene.py b/tests/test_functions_scene.py
new file mode 100644
index 0000000..7a5b78b
--- /dev/null
+++ b/tests/test_functions_scene.py
@@ -0,0 +1,76 @@
+import pytest
+
+from blc2.functions.scene import Scene
+from blc2.constants import INFTY
+
+def test_scene(aws):
+ """Test creation and modification of scenes."""
+ c1, c2, c3, c4 = aws.fixtures[0].channels
+
+ s1 = Scene(aws)
+ assert s1.scope == frozenset()
+ assert s1.scope == frozenset()
+
+ values = {c1: 0, c2: 1, c3: 2, c4: 4}
+ s1.values = values
+ assert s1.values == values
+ assert s1.scope == frozenset(values.keys())
+
+ s1.update({c1: 10})
+ assert s1.values[c1] == 10
+ assert s1.scope == frozenset(values.keys())
+ for c, v in values.items():
+ if c == c1:
+ continue
+ assert v == s1.values[c]
+
+ s1.set(values)
+ assert s1.values == values
+ assert s1.scope == frozenset(values.keys())
+
+ del s1[c1]
+ assert s1.scope == {c2, c3, c4}
+ assert c1 not in s1.values
+ assert s1[c1] is None
+
+ s1[c1] = 10
+ assert s1.scope == frozenset(values.keys())
+ assert s1[c1] == 10
+
+ assert s1.duration == INFTY
+ assert s1.actual_duration == INFTY
+ assert s1.fade_in == 0
+ assert s1.fade_out == 0
+ assert not s1.audio_scope
+
+ with pytest.raises(Exception):
+ s1.duration = 0
+
+ with pytest.raises(Exception):
+ s1.actual_duration = 0
+
+ with pytest.raises(Exception):
+ s1.fade_in = 0
+
+ with pytest.raises(Exception):
+ s1.fade_out = 0
+
+ with pytest.raises(Exception):
+ s1.scope = frozenset()
+
+ with pytest.raises(Exception):
+ s1.audio_scope = frozenset()
+
+ s1.set(values)
+ lc, ac, _ = s1.render(0)
+ assert ac == ()
+ assert dict(lc) == values
+
+ lc, ac, _ = s1.render(1234567890)
+ assert ac == ()
+ assert dict(lc) == values
+
+ s1[c1] = 10
+ lc, ac, _ = s1.render(1)
+ assert dict(lc)[c1] == 10
+ assert False not in (v == values[c] for c, v in lc if c != c1)
diff --git a/tests/test_topology.py b/tests/test_topology.py
new file mode 100644
index 0000000..0579d63
--- /dev/null
+++ b/tests/test_topology.py
@@ -0,0 +1,55 @@
+"""Module for testing topology behaviour."""
+
+import pytest
+
+from blc2.topology import Fixture
+from blc2.workspace import Workspace
+
+def test_fixture_create(ws):
+ """Test basic fixture creation."""
+ f = Fixture(ws, id_=0, name="Test 1")
+ assert f.id in ws.fixtures
+ assert f.id == 0
+ assert f.name == "Test 1"
+ assert not f.channels
+
+ with pytest.raises(ValueError):
+ Fixture(ws, id_=0, name="Test 2")
+
+ assert ws.fixtures[0] == f
+ assert len(ws.fixtures) == 1
+
+ f2 = Fixture(ws, name="Test 3")
+ assert f2.id is not None
+ assert ws.fixtures[f2.id] == f2
+ assert len(ws.fixtures) == 2
+
+ f3 = Fixture(ws, channel_count = 3)
+ assert len(f3.channels) == 3
+
+def test_fixture_channels(ws):
+ """Test fixture channel manipulations."""
+ f = Fixture(ws, channel_count=3)
+ for i in range(3):
+ f.channels[i].address = (0, i)
+ channels = tuple(f.channels)
+
+ with pytest.raises(ValueError):
+ f.channel_count = -1
+ assert len(f.channels) == 3
+
+ ## Verify that the channels haven't been changed
+ for a, b in zip(f.channels, channels):
+ assert id(a) == id(b)
+
+ f.channel_count = 2
+ assert len(f.channels) == 2
+ for a, b in zip(f.channels, channels[:2]):
+ assert id(a) == id(b)
+
+ f.channel_count = 3
+ assert len(f.channels) == 3
+ for a, b in zip(f.channels, channels[:2]):
+ assert id(a) == id(b)
+
+ assert id(f.channels[2]) != id(channels[2])