fix return-value/deprecated API bugs, accumulation lock leaks, code cleanup
Build Debian Packages / build (bookworm, debian12) (push) Successful in 8m56s
Build Debian Packages / build (trixie, debian13) (push) Successful in 9m3s

- raise instead of silent None for unsupported operands and empty negation
- validate before acquiring lock in Accumulation operators (no RLock leak)
- Accumulation._copy_provenance helper, ADC_Result._copy_metadata for all result paths
- shared _sandbox_stdout_redirect contextmanager for sandboxed scripts
- TemperatureMonitor dead code removal (gtk helper, traceback, unused properties)
- new unit tests incl. lock-freedom regression test
- gitignore sphinx generated doc/_autosummary/ and doc/damaris/
This commit is contained in:
markusro committed 2026-10-17 19:17:43 +02:00
1 parent 91ade4376c
commit f9d8de7697
26 files changed
+590 -236

No files matched your search

+4 -3
View File
@@ -11,6 +11,7 @@ def test(i):
return e
def experiment():
for i in range(1400):
sleep(0.1)
yield test(i)
for i in range(100):
#sleep(0.1)
yield test(i)
synchronize()
+2
View File
@@ -9,6 +9,7 @@ def result():
except:
print("h5 already opened")
accu = Accumulation(error=False)
for num,ts in enumerate(results):
data["accu"] = accu
data["data/ts %i"%num] = ts+0
@@ -17,6 +18,7 @@ def result():
r[num] = ts.y[0][0]
data["test"] = r
accu+=ts
print(accu)
accu.write_to_hdf(h5, "/", "name", "title")
try:
h5.close()
+38 -11
View File
@@ -195,14 +195,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]+10, y[i] + 10, strict=True)
adc += 10
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] + 10, strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] + 10).astype("float32"), strict=True)
# test float addition
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] + 10., y[i] + 10., strict=True)
np.testing.assert_array_equal(adc.y[i] + 10., (y[i] + 10 + 10.).astype("float32"), strict=True)
adc += 10.
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] + 10 + 10., strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] + 10 + 10.).astype("float32"), strict=True)
def test_operator_sub_scalar(self):
"""
@@ -215,14 +215,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i] - 10, y[i] - 10, strict=True)
adc -= 10
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] - 10, strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] - 10).astype("float32"), strict=True)
# test float subtraction
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] - 10., y[i] - 10., strict=True)
np.testing.assert_array_equal(adc.y[i] - 10., (y[i] - 10 - 10.).astype("float32"), strict=True)
adc -= 10.
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] - 10 - 10., strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] - 10 - 10.).astype("float32"), strict=True)
def test_operator_mul_scalar(self):
"""
@@ -236,14 +236,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i] * 10, y[i] * 10, strict=True)
adc *= 10
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] * 10, strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] * 10).astype("float32"), strict=True)
# test float multiplication
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] * 10.0, y[i] * 10.0, strict=True)
np.testing.assert_array_equal(adc.y[i] * 10.0, (y[i] * 10 * 10.0).astype("float32"), strict=True)
adc *= 10.0
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] * 10 * 10.0, strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] * 10 * 10.0).astype("float32"), strict=True)
def test_operator_truediv_scalar(self):
"""
@@ -255,7 +255,7 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]/10, y[i] / 10, strict=True)
adc /= 10
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] / 10, strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] / 10).astype("float32"), strict=True)
def test_operator_floordiv_scalar(self):
"""
@@ -267,10 +267,37 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]//10, y[i] // 10, strict=True)
adc //= 10
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] // 10, strict=True)
np.testing.assert_array_equal(adc.y[i], (y[i] // 10).astype("float32"), strict=True)
self.assertRaises(ValueError, adc.__floordiv__, 1.0)
self.assertRaises(ValueError, adc.__rfloordiv__, 1.0)
def test_operators_keep_channel_metadata(self):
"""operator results keep nChannels/is_clipped identical to the operand"""
adc = self.create_adc_result()
adc.is_clipped = True
results = [adc + 1, adc - 1, adc * 2, adc ** 2, adc / 2., adc // 2,
10 + adc, 10 - adc, 2 * adc, 100. / adc, 100 // adc, -adc]
for r in results:
self.assertEqual(r.get_nChannels(), adc.get_nChannels())
self.assertEqual(r.is_clipped, adc.is_clipped)
def test_hdf_roundtrip_keeps_nchannels(self):
"""read_from_hdf must restore the number of channels"""
import tempfile
import tables
from damaris.data.ADC_Result import read_from_hdf
adc = self.create_adc_result()
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "adc.h5")
with tables.open_file(path, "w") as h5:
adc.write_to_hdf(h5, h5.root, "adc", "test")
with tables.open_file(path, "r") as h5:
adc2 = read_from_hdf(h5.root.adc)
self.assertIsNotNone(adc2)
self.assertTrue(adc2.contains_data())
self.assertEqual(adc2.get_nChannels(), adc.get_nChannels())
def test_operator_overload_adc_result_exception(self):
"""
Test the ValueError raised when ADC_Result is used as an operand in an arithmetic operation
+71 -1
View File
@@ -1,5 +1,6 @@
import os
import subprocess
import threading
import unittest
from datetime import datetime
@@ -258,7 +259,7 @@ class TestAccumulation(unittest.TestCase):
np.testing.assert_array_equal(accu.y[i], y[i] - 10)
# test float subtraction
adc -= 10.
accu -= 10.
for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(accu.y[i], y[i] - 10 - 10.)
@@ -312,5 +313,74 @@ class TestAccumulation(unittest.TestCase):
self.assertRaises(ValueError, accu.__floordiv__, 1.0)
self.assertRaises(ValueError, accu.__ifloordiv__, 1.0)
def test_add_invalid_operand_raises(self):
"""accu + bogus must raise ValueError instead of returning None."""
adc = self.create_adc_result()
accu = Accumulation()
accu += adc
self.assertRaises(ValueError, accu.__add__, "foo")
self.assertRaises(ValueError, accu.__iadd__, "foo")
def test_neg_empty_raises(self):
"""-empty accumulation must raise ValueError."""
self.assertRaises(ValueError, Accumulation().__neg__)
def test_add_adc_result_does_not_modify_job_ids(self):
"""accu + adc must not insert the adc job id into accu.job_ids."""
accu = Accumulation()
accu += self.create_adc_result()
n_before = len(accu.job_ids)
adc2 = self.create_adc_result()
adc2.job_id = 12
r = accu + adc2
self.assertIn(12, r.job_ids)
self.assertNotIn(12, accu.job_ids)
self.assertEqual(len(accu.job_ids), n_before)
def test_empty_plus_accumulation_copies_data(self):
"""empty_accu + accu must copy arrays and metadata, not alias them."""
adc = self.create_adc_result()
accu = Accumulation()
accu += adc
accu.is_clipped = True
r = Accumulation() + accu
self.assertIsNot(r.y[0], accu.y[0])
self.assertIsNot(r.job_ids, accu.job_ids)
self.assertTrue(r.is_clipped)
# in-place operation on the result must leave the operand unchanged
r *= 2
np.testing.assert_array_equal(accu.y[0], np.array([10, 20, 30], dtype="float32"))
def test_accumulation_plus_accumulation_keeps_both_job_ids(self):
"""accu1 + accu2 must keep the provenance of both operands."""
adc1 = self.create_adc_result()
adc2 = self.create_adc_result()
adc2.job_id = 12
accu1 = Accumulation()
accu1 += adc1
accu2 = Accumulation()
accu2 += adc2
r = accu1 + accu2
self.assertEqual(set(r.job_ids.keys()), {11, 12})
self.assertEqual(set(accu1.job_ids.keys()), {11})
self.assertEqual(set(accu2.job_ids.keys()), {12})
def test_empty_error_plus_non_error_raises(self):
"""error accumulation + non-error accumulation must raise ValueError."""
accu = Accumulation()
accu += self.create_adc_result()
empty_err = Accumulation(error=True)
self.assertRaises(ValueError, empty_err.__add__, accu)
free = []
def check_lock():
if empty_err.lock.acquire(timeout=1):
free.append(True)
empty_err.lock.release()
t = threading.Thread(target=check_lock)
t.start()
t.join(3)
self.assertTrue(free, "lock leaked after ValueError in __add__")
if __name__ == "__main__":
unittest.main()
+29
View File
@@ -0,0 +1,29 @@
import unittest
from damaris.data.Config_Result import Config_Result
class TestConfigResult(unittest.TestCase):
def test_constructor_with_no_arguments(self):
"""Test initializer with no arguments."""
cr = Config_Result()
self.assertEqual(cr.get_config_dictionary(), {})
self.assertEqual(cr.description, {})
def test_constructor_with_arguments(self):
"""Test initializer with config and description arguments."""
config = {"a": 1}
desc = {"b": 2}
cr = Config_Result(config=config, desc=desc)
self.assertEqual(cr.get_config_dictionary(), config)
self.assertEqual(cr.description, desc)
def test_get_set_config(self):
cr = Config_Result()
cr.set_config("key", "value")
self.assertEqual(cr.get_config("key"), "value")
self.assertIsNone(cr.get_config("missing"))
if __name__ == "__main__":
unittest.main()
+44
View File
@@ -0,0 +1,44 @@
import unittest
import numpy as np
from damaris.data.DamarisFFT import DamarisFFT
class TestDamarisFFT(unittest.TestCase):
@staticmethod
def make_fft():
fft = DamarisFFT()
fft.sampling_rate = 100.0
fft.x = np.arange(10, dtype="float32") / 100.0
fft.y = [np.arange(10, dtype="float32")]
fft.xlabel = "Time / s"
return fft
def test_clip_without_arguments_is_placeholder(self):
"""clip() without arguments returns self unchanged (placeholder use)."""
fft = self.make_fft()
self.assertIs(fft.clip(), fft)
self.assertEqual(len(fft.x), 10)
np.testing.assert_array_equal(fft.y[0], np.arange(10, dtype="float32"))
def test_clip_swaps_reversed_bounds(self):
fft = self.make_fft()
fft.clip(start=0.05, stop=0.01)
expected_x = (np.arange(10, dtype="float32") / 100.0)[1:5]
np.testing.assert_array_equal(fft.x, expected_x)
np.testing.assert_array_equal(fft.y[0], np.arange(1, 5, dtype="float32"))
def test_clip_single_bound(self):
fft = self.make_fft()
fft.clip(start=0.02)
self.assertEqual(len(fft.x), 7)
self.assertEqual(len(fft.y[0]), 7)
def test_clip_too_close_raises(self):
fft = self.make_fft()
self.assertRaises(ValueError, fft.clip, 0.021, 0.029)
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
import sys
import threading
import unittest
from damaris.gui.ExperimentHandling import ExperimentHandling
from damaris.gui.ResultHandling import ResultHandling
class _FakeWriter:
def __init__(self):
self.sent = []
def send_next(self, job, quit=False):
self.sent.append(job)
class _FakeResultIterator:
def __init__(self):
self.quit_flag = threading.Event()
def __iter__(self):
return iter([])
class TestSandboxStdoutRestore(unittest.TestCase):
"""
sys.stdout/sys.stderr are process-global: the sandbox redirect in
run() must be undone even when the script raises or returns early.
"""
def test_experiment_stdout_restored_after_script_exception(self):
orig_stdout = sys.stdout
orig_stderr = sys.stderr
handled = ExperimentHandling("raise ValueError('boom')", None, None,
log_callback=lambda line: None)
handled.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIs(sys.stderr, orig_stderr)
self.assertIsNotNone(handled.raised_exception)
def test_experiment_stdout_restored_on_success(self):
orig_stdout = sys.stdout
writer = _FakeWriter()
handled = ExperimentHandling("print('hello')\nexperiment = lambda: iter([])",
writer, None, log_callback=lambda line: None)
handled.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIsNone(handled.raised_exception)
def test_result_stdout_restored_after_script_exception(self):
orig_stdout = sys.stdout
orig_stderr = sys.stderr
handling = ResultHandling("raise ValueError('boom')", _FakeResultIterator(),
None, log_callback=lambda line: None)
handling.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIs(sys.stderr, orig_stderr)
self.assertIsNotNone(handling.raised_exception)
def test_result_stdout_restored_without_result_function(self):
orig_stdout = sys.stdout
handling = ResultHandling("x = 1", _FakeResultIterator(), None,
log_callback=lambda line: None)
handling.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIsNone(handling.raised_exception)
if __name__ == "__main__":
unittest.main()
+35
View File
@@ -0,0 +1,35 @@
import unittest
import numpy as np
from damaris.data.MeasurementResult import MeasurementResult
class TestMeasurementResult(unittest.TestCase):
def test_add_zero_preserves_data(self):
"""mr + 0 must return a MeasurementResult with identical data."""
mr = MeasurementResult("test_quantity")
for i in range(3):
mr[i] = float(i * i)
mr2 = mr + 0
self.assertIsInstance(mr2, MeasurementResult)
self.assertEqual(mr2.quantity_name, "test_quantity")
np.testing.assert_array_equal(mr2.get_xdata(), mr.get_xdata())
np.testing.assert_array_equal(mr2.get_ydata(), mr.get_ydata())
def test_add_zero_returns_independent_copy(self):
"""Changing the copy must not change the original."""
mr = MeasurementResult("q")
mr[1.0] = 5.0
mr2 = mr + 0
self.assertIsNot(mr2[1.0], mr[1.0])
mr2[1.0] = mr2[1.0] + 100.0
self.assertEqual(mr[1.0].mean(), 5.0)
def test_add_nonzero_raises(self):
mr = MeasurementResult("q")
self.assertRaises(Exception, mr.__add__, 1)
if __name__ == "__main__":
unittest.main()