summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.coveragerc21
-rw-r--r--blc2/constants.py14
-rw-r--r--blc2/functions/audio.py1
-rw-r--r--blc2/functions/chaser.py14
-rw-r--r--blc2/functions/chaserstep.py15
-rw-r--r--blc2/workspace.py7
-rw-r--r--examples/workspace.xml6
-rw-r--r--tests/conftest.py15
-rw-r--r--tests/test_constants.py20
-rw-r--r--tests/test_functions_audio.py27
-rw-r--r--tests/test_functions_scene.py38
11 files changed, 155 insertions, 23 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..088c2da
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,21 @@
+# .coveragerc to control coverage.py
+[run]
+branch = True
+
+[report]
+# Regexes for lines to exclude from consideration
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+
+ # Don't complain about missing debug-only code:
+ def __repr__
+ if self\.debug
+
+ # Don't complain if tests don't hit defensive assertion code:
+ raise AssertionError
+ raise NotImplementedError
+
+ # Don't complain if non-runnable code isn't run:
+ if 0:
+ if __name__ == .__main__.:
diff --git a/blc2/constants.py b/blc2/constants.py
index ad6d174..bb207f9 100644
--- a/blc2/constants.py
+++ b/blc2/constants.py
@@ -18,8 +18,16 @@ class _Infinity:
return "infty"
def __gt__(self, other):
+ if other == INFTY:
+ return False
return True
+ def __ge__(self, other):
+ return True
+
+ def __le__(self, other):
+ return other == INFTY
+
def __lt__(self, other):
return False
@@ -37,13 +45,11 @@ class _Infinity:
def __mul__(self, other):
if other == 0:
- return 0
+ raise ValueError("Undefined")
return self
def __rmul__(self, other):
- if other == 0:
- return 0
- return self
+ return self * other
def __init__(self):
if _Infinity.INFTY is not None:
diff --git a/blc2/functions/audio.py b/blc2/functions/audio.py
index 42cd3f6..9b22ba9 100644
--- a/blc2/functions/audio.py
+++ b/blc2/functions/audio.py
@@ -30,6 +30,7 @@ class Audio(Function):
self._audio_id = self.id
if fade_in < 0 or fade_out < 0:
+ self.w.function_deleted(self)
raise ValueError("Fades must be nonnegative")
self._fade_out = fade_out
self._fade_in = fade_in
diff --git a/blc2/functions/chaser.py b/blc2/functions/chaser.py
index a6699f8..fe6fc17 100644
--- a/blc2/functions/chaser.py
+++ b/blc2/functions/chaser.py
@@ -112,6 +112,7 @@ class Chaser(Function):
def next_steps(self, data):
"""Iterate over the next steps in the chaser."""
n = data.steps[-1].index
+ print("Last index", n)
if self._advance_mode == RANDOM:
while True:
yield random.randint(0, len(self.steps)-1)
@@ -214,31 +215,30 @@ class Chaser(Function):
data.audio_id += 1
## Make sure we have all the steps we need
- st = data.steps[-1].duration + data.steps[-1].start_time
+ st = data.steps[-1].step.duration + data.steps[-1].start_time
if st < INFTY and st <= t:
for n in self.next_steps(data):
s = self.steps[n]
- st += s.duration
- et += s.actual_duration
+ et = st + s.actual_duration
if et >= t:
## We need this one
- data.steps.append(self.steps[n]._get_data(0, data.audio_id)) #pylint: disable=protected-access
+ data.steps.append(self.steps[n]._get_data(st, data.audio_id)) #pylint: disable=protected-access
data.audio_id += 1
+ i = data.steps[-1]
+ st += s.duration
if st > t:
## We're done
break
## Clean up the old steps
data.steps = [i for i in data.steps if i.start_time+i.actual_duration >= t and i.step.index != -1]
- if not data.steps:
- return (), (), data
## Render
lc = {c: 0 for c in self._scope}
ac = []
for n, d in enumerate(data.steps):
s = d.step
- slc, sac, data.steps[n] = s.render(t, data=data.steps[n])
+ slc, sac, data.steps[n] = s.render(t, data=d)
lc.update(slc)
ac.extend(sac)
diff --git a/blc2/functions/chaserstep.py b/blc2/functions/chaserstep.py
index 005f76e..597b760 100644
--- a/blc2/functions/chaserstep.py
+++ b/blc2/functions/chaserstep.py
@@ -231,11 +231,10 @@ class ChaserStep(Function):
if data.index != self._index:
data.index = self._index
- if t > min(self._actual_duration, data.end_time+self._fade_out):
- return (), (), data
-
fade_start = min(data.end_time, self._duration)
t -= data.start_time
+ if t > min(self._actual_duration, data.end_time+self._fade_out):
+ return (), (), data
## Compute lighting multiplier
mul = 1
@@ -282,7 +281,15 @@ class ChaserStep(Function):
duration_mode = e.get("duration-mode")
if duration_mode not in (MANUAL, INHERIT):
raise LoadError("Invalid duration mode")
- duration = int(e.get("duration")) if duration_mode == MANUAL else 0
+ if duration_mode == INHERIT:
+ duration = 0
+ else:
+ duration = e.get("duration")
+ if duration.strip().lower() == "infty":
+ duration = INFTY
+ else:
+ duration = int(duration)
+
function = int(e.get("function"))
return cls(c, id_=id_, name=name, fade_in=fade_in, fade_out=fade_out,
diff --git a/blc2/workspace.py b/blc2/workspace.py
index 83dba5b..2f2cf79 100644
--- a/blc2/workspace.py
+++ b/blc2/workspace.py
@@ -195,10 +195,9 @@ 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)
+ if f.id in self._change_callbacks:
+ for _, callback in self._delete_callbacks[f.id]:
+ callback(f)
self.delete_callbacks(f)
diff --git a/examples/workspace.xml b/examples/workspace.xml
index c0161fb..3fa2d70 100644
--- a/examples/workspace.xml
+++ b/examples/workspace.xml
@@ -32,11 +32,13 @@
</function>
<function type="Audio" id="1" name="Intro" fade-in="0" fade-out="0">
- <filename>test.wav</filename>
+ <filename>tests/silence.m4a</filename>
</function>
<function type="Chaser" id="2" name="Chaser 1" advance-mode="Loop">
- <step id="3" name="Step 1" fade-in="0" fade-out="0" duration-mode="Manual" duration="123" function="0"/>
+ <step id="3" name="Step 1" fade-in="10" fade-out="100" duration-mode="Manual" duration="infty" function="0"/>
+ <step id="4" name="Step 2" fade-in="500" fade-out="500" duration-mode="Inherit" function="1"/>
</function>
+
</functions>
</workspace>
diff --git a/tests/conftest.py b/tests/conftest.py
index 71bd3ac..1a9af58 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,5 @@
import datetime as dt
+import xml.etree.ElementTree as et
import pytest
@@ -16,3 +17,17 @@ def aws():
w = Workspace("", "", "", dt.datetime.now())
Fixture(w, id_=0, channel_count=4)
return w
+
+def elements_equal(e1, e2):
+ if e1.tag != e2.tag: return False
+ if e1.text != e2.text: return False
+ if e1.tail.strip() != e2.tail.strip(): return False
+ if e1.attrib != e2.attrib: return False
+ if len(e1) != len(e2): return False
+ return all(elements_equal(c1, c2) for c1, c2 in zip(e1, e2))
+
+@pytest.fixture
+def test_xml_eq():
+ def inner(e: et.Element, s: str):
+ return elements_equal(e, et.fromstring(s))
+ return inner
diff --git a/tests/test_constants.py b/tests/test_constants.py
new file mode 100644
index 0000000..5ce0b1a
--- /dev/null
+++ b/tests/test_constants.py
@@ -0,0 +1,20 @@
+"""Tests for the constants used.
+
+Mainly just tests the INFTY object.
+"""
+
+import pytest
+
+from blc2.constants import INFTY, _Infinity
+
+def test_infty():
+ assert str(INFTY) == "infty"
+ assert 1+INFTY == INFTY+1 == INFTY
+ assert 1-INFTY == INFTY-1 == INFTY
+ assert 4*INFTY == INFTY*4 == INFTY*0.4 == INFTY
+
+ with pytest.raises(Exception):
+ _ = INFTY * 0
+
+ with pytest.raises(Exception):
+ infty2 = _Infinity()
diff --git a/tests/test_functions_audio.py b/tests/test_functions_audio.py
index de6aa2a..35d548a 100644
--- a/tests/test_functions_audio.py
+++ b/tests/test_functions_audio.py
@@ -1,4 +1,6 @@
-import os
+"""Tests for the audio primitive."""
+
+import pytest
from blc2.functions.audio import Audio
@@ -15,6 +17,7 @@ def test_audio(aws):
assert a.actual_duration == 0
a.filename = "tests/silence.m4a"
+ assert a.filename == "tests/silence.m4a"
assert a.audio_scope == {"tests/silence.m4a"}
assert a.duration == 2024
assert a.actual_duration == 3024
@@ -34,3 +37,25 @@ def test_audio(aws):
_, ac, _ = a.render(5000)
assert not ac
+
+ with pytest.raises(ValueError):
+ _ = Audio(aws, id_=101, fade_in=-10)
+
+ assert 101 not in aws.functions
+
+ fout = a.fade_out
+ with pytest.raises(ValueError):
+ a.fade_out = -50
+ assert a.fade_out == fout
+
+ fin = a.fade_in
+ with pytest.raises(ValueError):
+ a.fade_in = -50
+ assert a.fade_in == fin
+
+ a._set_duration(10000)
+ assert a.copy_data(a.get_data()) is None
+
+ a.filename = None
+ assert a.duration == 0
+ assert a.actual_duration == 0
diff --git a/tests/test_functions_scene.py b/tests/test_functions_scene.py
index 7a5b78b..77b2d6f 100644
--- a/tests/test_functions_scene.py
+++ b/tests/test_functions_scene.py
@@ -1,7 +1,9 @@
import pytest
+import xml.etree.ElementTree as et
+
from blc2.functions.scene import Scene
-from blc2.constants import INFTY
+from blc2.constants import INFTY, BXW
def test_scene(aws):
"""Test creation and modification of scenes."""
@@ -74,3 +76,37 @@ def test_scene(aws):
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)
+
+ with pytest.raises(ValueError):
+ s1[c1] = -10
+ assert s1[c1] == 10
+
+ with pytest.raises(ValueError):
+ s1[c1] = -256
+ assert s1[c1] == 10
+
+ del s1[c4]
+ del s1[c4]
+
+def test_scene_serialize(aws, test_xml_eq):
+ return
+ c1, c2, c3, c4 = aws.fixtures[0].channels
+ s = Scene(aws, id_=123, name="Test Scene", values={c1: 1, c2: 2, c3: 3, c4: 4})
+
+ (f1, i1), (f2, i2), (f3, i3), (f4, i4) = ((i.f.id, i.id) for i in (c1, c2, c3, c4))
+
+ e = s.serialize()
+ expected = """
+<function xmlns="{bxw}" type="Scene" id="123" name="Test Scene">
+ <value fixture="{f1}" channel="{i1}">1</value>
+ <value fixture="{f2}" channel="{i2}">2</value>
+ <value fixture="{f3}" channel="{i3}">3</value>
+ <value fixture="{f4}" channel="{i4}">4</value>
+</function>
+""".format(bxw=BXW.strip("{}"), f1=f1, f2=f2, f3=f3, f4=f4, i1=i1,i2=i2,i3=i3,i4=i4)
+
+ et.register_namespace("", BXW)
+ print(et.tostring(e, encoding="utf-8"))
+ print(expected)
+
+ assert test_xml_eq(e, expected)