Skip to content

Commit 2b0621b

Browse files
authored
Bug/capture merge (#141)
* fix bug on merging captures * improve test organization
1 parent c06863d commit 2b0621b

File tree

3 files changed

+96
-64
lines changed

3 files changed

+96
-64
lines changed

sigmf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# SPDX-License-Identifier: LGPL-3.0-or-later
66

77
# version of this python module
8-
__version__ = "1.7.0"
8+
__version__ = "1.7.1"
99
# matching version of the SigMF specification
1010
__specification__ = "1.2.6"
1111

sigmf/sigmffile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,9 +507,9 @@ def add_capture(self, start_index, metadata=None):
507507
new_capture[self.START_INDEX_KEY] = start_index
508508
# merge if capture exists
509509
merged = False
510-
for existing_capture in self._metadata[self.CAPTURE_KEY]:
510+
for idx, existing_capture in enumerate(self._metadata[self.CAPTURE_KEY]):
511511
if existing_capture[self.START_INDEX_KEY] == start_index:
512-
existing_capture = dict_merge(existing_capture, new_capture)
512+
self._metadata[self.CAPTURE_KEY][idx] = dict_merge(existing_capture, new_capture)
513513
merged = True
514514
if not merged:
515515
capture_list += [new_capture]

tests/test_sigmffile.py

Lines changed: 93 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ def test_equality(self):
8484
class TestAnnotationHandling(unittest.TestCase):
8585
def test_get_annotations_with_index(self):
8686
"""Test that only annotations containing index are returned from get_annotations()"""
87-
smf = SigMFFile(copy.deepcopy(TEST_METADATA))
88-
smf.add_annotation(start_index=1)
89-
smf.add_annotation(start_index=4, length=4)
90-
annotations_idx10 = smf.get_annotations(index=10)
87+
meta = SigMFFile(copy.deepcopy(TEST_METADATA))
88+
meta.add_annotation(start_index=1)
89+
meta.add_annotation(start_index=4, length=4)
90+
annotations_idx10 = meta.get_annotations(index=10)
9191
self.assertListEqual(
9292
annotations_idx10,
9393
[
@@ -96,26 +96,26 @@ def test_get_annotations_with_index(self):
9696
],
9797
)
9898

99-
def test__count_samples_from_annotation(self):
99+
def test_sample_count_from_annotations(self):
100100
"""Make sure sample count from annotations use correct end index"""
101-
smf = SigMFFile(copy.deepcopy(TEST_METADATA))
102-
smf.add_annotation(start_index=0, length=32)
103-
smf.add_annotation(start_index=4, length=4)
104-
sample_count = smf._count_samples()
101+
meta = SigMFFile(copy.deepcopy(TEST_METADATA))
102+
meta.add_annotation(start_index=0, length=32)
103+
meta.add_annotation(start_index=4, length=4)
104+
sample_count = meta._count_samples()
105105
self.assertEqual(sample_count, 32)
106106

107107
def test_set_data_file_without_annotations(self):
108108
"""
109109
Make sure setting data_file with no annotations registered does not
110110
raise any errors
111111
"""
112-
smf = SigMFFile(copy.deepcopy(TEST_METADATA))
113-
smf._metadata[SigMFFile.ANNOTATION_KEY].clear()
112+
meta = SigMFFile(copy.deepcopy(TEST_METADATA))
113+
meta._metadata[SigMFFile.ANNOTATION_KEY].clear()
114114
with tempfile.TemporaryDirectory() as tmpdir:
115115
temp_path_data = Path(tmpdir) / "datafile"
116116
TEST_FLOAT32_DATA.tofile(temp_path_data)
117-
smf.set_data_file(temp_path_data)
118-
samples = smf.read_samples()
117+
meta.set_data_file(temp_path_data)
118+
samples = meta.read_samples()
119119
self.assertTrue(len(samples) == 16)
120120

121121
def test_set_data_file_with_annotations(self):
@@ -124,15 +124,15 @@ def test_set_data_file_with_annotations(self):
124124
count from data_file and issue a warning if annotations have end
125125
indices bigger than file end index
126126
"""
127-
smf = SigMFFile(copy.deepcopy(TEST_METADATA))
128-
smf.add_annotation(start_index=0, length=32)
127+
meta = SigMFFile(copy.deepcopy(TEST_METADATA))
128+
meta.add_annotation(start_index=0, length=32)
129129
with tempfile.TemporaryDirectory() as tmpdir:
130130
temp_path_data = Path(tmpdir) / "datafile"
131131
TEST_FLOAT32_DATA.tofile(temp_path_data)
132132
with self.assertWarns(Warning):
133133
# Issues warning since file ends before the final annotatio
134-
smf.set_data_file(temp_path_data)
135-
samples = smf.read_samples()
134+
meta.set_data_file(temp_path_data)
135+
samples = meta.read_samples()
136136
self.assertTrue(len(samples) == 16)
137137

138138

@@ -222,9 +222,9 @@ def test_key_validity():
222222

223223
def test_ordered_metadata():
224224
"""check to make sure the metadata is sorted as expected"""
225-
sigf = SigMFFile()
225+
meta = SigMFFile()
226226
top_sort_order = ["global", "captures", "annotations"]
227-
for kdx, key in enumerate(sigf.ordered_metadata()):
227+
for kdx, key in enumerate(meta.ordered_metadata()):
228228
assert kdx == top_sort_order.index(key)
229229

230230

@@ -249,7 +249,7 @@ def prepare(self, data: list, meta: dict, dtype: type, autoscale: bool = True) -
249249
meta = sigmf.fromfile(self.temp_path_meta, skip_checksum=True, autoscale=autoscale)
250250
return meta
251251

252-
def test_000(self) -> None:
252+
def test_compliant_two_capture_recording(self) -> None:
253253
"""compliant two-capture recording"""
254254
meta = self.prepare(TEST_U8_DATA0, TEST_U8_META0, np.uint8, autoscale=False)
255255
self.assertEqual(256, meta._count_samples())
@@ -260,7 +260,7 @@ def test_000(self) -> None:
260260
self.assertTrue(np.array_equal(np.array([]), meta.read_samples_in_capture(0)))
261261
self.assertTrue(np.array_equal(TEST_U8_DATA0, meta.read_samples_in_capture(1)))
262262

263-
def test_001(self) -> None:
263+
def test_two_capture_with_header_trailing_bytes(self) -> None:
264264
"""two capture recording with header_bytes and trailing_bytes set"""
265265
meta = self.prepare(TEST_U8_DATA1, TEST_U8_META1, np.uint8, autoscale=False)
266266
self.assertEqual(192, meta._count_samples())
@@ -270,7 +270,7 @@ def test_001(self) -> None:
270270
self.assertTrue(np.array_equal(np.arange(128), meta.read_samples_in_capture(0)))
271271
self.assertTrue(np.array_equal(np.arange(128, 192), meta.read_samples_in_capture(1)))
272272

273-
def test_002(self) -> None:
273+
def test_two_capture_with_multiple_header_bytes(self) -> None:
274274
"""two capture recording with multiple header_bytes set"""
275275
meta = self.prepare(TEST_U8_DATA2, TEST_U8_META2, np.uint8, autoscale=False)
276276
self.assertEqual(192, meta._count_samples())
@@ -280,7 +280,7 @@ def test_002(self) -> None:
280280
self.assertTrue(np.array_equal(np.arange(128), meta.read_samples_in_capture(0)))
281281
self.assertTrue(np.array_equal(np.arange(128, 192), meta.read_samples_in_capture(1)))
282282

283-
def test_003(self) -> None:
283+
def test_three_capture_with_multiple_header_bytes(self) -> None:
284284
"""three capture recording with multiple header_bytes set"""
285285
meta = self.prepare(TEST_U8_DATA3, TEST_U8_META3, np.uint8, autoscale=False)
286286
self.assertEqual(192, meta._count_samples())
@@ -292,8 +292,8 @@ def test_003(self) -> None:
292292
self.assertTrue(np.array_equal(np.arange(32, 128), meta.read_samples_in_capture(1)))
293293
self.assertTrue(np.array_equal(np.arange(128, 192), meta.read_samples_in_capture(2)))
294294

295-
def test_004(self) -> None:
296-
"""two channel version of 000"""
295+
def test_two_channel_capture_recording(self) -> None:
296+
"""two channel version of compliant capture recording"""
297297
meta = self.prepare(TEST_U8_DATA4, TEST_U8_META4, np.uint8, autoscale=False)
298298
self.assertEqual(96, meta._count_samples())
299299
self.assertFalse(meta._is_conforming_dataset())
@@ -302,20 +302,20 @@ def test_004(self) -> None:
302302
self.assertTrue(np.array_equal(np.arange(64).repeat(2).reshape(-1, 2), meta.read_samples_in_capture(0)))
303303
self.assertTrue(np.array_equal(np.arange(64, 96).repeat(2).reshape(-1, 2), meta.read_samples_in_capture(1)))
304304

305-
def test_slicing_ru8(self) -> None:
305+
def test_slice_real_uint8(self) -> None:
306306
"""slice real uint8"""
307307
meta = self.prepare(TEST_U8_DATA0, TEST_U8_META0, np.uint8, autoscale=False)
308308
self.assertTrue(np.array_equal(meta[:], TEST_U8_DATA0))
309309
self.assertTrue(np.array_equal(meta[6], TEST_U8_DATA0[6]))
310310
self.assertTrue(np.array_equal(meta[1:-1], TEST_U8_DATA0[1:-1]))
311311

312-
def test_slicing_rf32(self) -> None:
312+
def test_slice_real_float32(self) -> None:
313313
"""slice real float32"""
314314
meta = self.prepare(TEST_FLOAT32_DATA, TEST_METADATA, np.float32)
315315
self.assertTrue(np.array_equal(meta[:], TEST_FLOAT32_DATA))
316316
self.assertTrue(np.array_equal(meta[9], TEST_FLOAT32_DATA[9]))
317317

318-
def test_slicing_multiple_channels(self) -> None:
318+
def test_slice_multiple_channels(self) -> None:
319319
"""slice multiple channels"""
320320

321321
meta = self.prepare(TEST_U8_DATA4, TEST_U8_META4, np.uint8, autoscale=False)
@@ -325,7 +325,7 @@ def test_slicing_multiple_channels(self) -> None:
325325
self.assertTrue(np.array_equal(meta[0], channelized[0]))
326326
self.assertTrue(np.array_equal(meta[1, :], channelized[1]))
327327

328-
def test_boundaries(self) -> None:
328+
def test_capture_byte_boundaries(self) -> None:
329329
"""capture byte boundaries from pairs & archives"""
330330
# get a meta pair and archive
331331
meta = self.prepare(TEST_U8_DATA3, TEST_U8_META3, np.uint8)
@@ -336,6 +336,41 @@ def test_boundaries(self) -> None:
336336
self.assertEqual(meta.get_capture_byte_boundaries(bdx), arc.get_capture_byte_boundaries(bdx))
337337
self.assertTrue(np.array_equal(meta.read_samples_in_capture(bdx), arc.read_samples_in_capture(bdx)))
338338

339+
def test_add_capture(self):
340+
"""test basic capture addition"""
341+
meta = SigMFFile()
342+
meta.add_capture(start_index=0, metadata={})
343+
344+
def test_add_capture_metadata_merge(self):
345+
"""test that adding capture with existing start_index properly merges metadata"""
346+
meta = SigMFFile()
347+
348+
# add initial capture with some metadata
349+
initial_meta = {"core:frequency": 915e6, "core:sample_rate": 1e6}
350+
meta.add_capture(start_index=0, metadata=initial_meta)
351+
352+
# add capture with same start_index but additional metadata
353+
additional_meta = {"core:datetime": "2026-03-17T10:00:00Z", "custom:gain": 30}
354+
meta.add_capture(start_index=0, metadata=additional_meta)
355+
356+
# verify metadata was merged properly
357+
captures = meta.get_captures()
358+
self.assertEqual(len(captures), 1, "should have exactly one capture")
359+
360+
merged_capture = captures[0]
361+
# original metadata should be preserved
362+
self.assertEqual(merged_capture["core:frequency"], 915e6)
363+
self.assertEqual(merged_capture["core:sample_rate"], 1e6)
364+
# new metadata should be added
365+
self.assertEqual(merged_capture["core:datetime"], "2026-03-17T10:00:00Z")
366+
self.assertEqual(merged_capture["custom:gain"], 30)
367+
368+
def test_add_multiple_captures_and_annotations(self):
369+
"""test adding multiple captures with annotations"""
370+
meta = SigMFFile()
371+
for idx in range(3):
372+
simulate_capture(meta, idx, 1024)
373+
339374

340375
def simulate_capture(sigmf_md, n, capture_len):
341376
start_index = capture_len * n
@@ -352,38 +387,35 @@ def simulate_capture(sigmf_md, n, capture_len):
352387
sigmf_md.add_annotation(start_index=start_index, length=capture_len, metadata=annotation_md)
353388

354389

355-
def test_default_constructor():
356-
SigMFFile()
357-
358-
359-
def test_set_non_required_global_field():
360-
sigf = SigMFFile()
361-
sigf.set_global_field("this_is:not_in_the_schema", None)
362-
363-
364-
def test_add_capture():
365-
sigf = SigMFFile()
366-
sigf.add_capture(start_index=0, metadata={})
367-
368-
369-
def test_add_annotation():
370-
sigf = SigMFFile()
371-
sigf.add_capture(start_index=0)
372-
meta = {"latitude": 40.0, "longitude": -105.0}
373-
sigf.add_annotation(start_index=0, length=128, metadata=meta)
374-
375-
376-
def test_fromarchive(test_sigmffile):
377-
with tempfile.NamedTemporaryFile(suffix=".sigmf") as temp_file:
378-
archive_path = test_sigmffile.archive(name=temp_file.name, overwrite=True)
379-
result = sigmf.fromarchive(archive_path=archive_path)
380-
assert result._metadata == test_sigmffile._metadata == TEST_METADATA
381-
382-
383-
def test_add_multiple_captures_and_annotations():
384-
sigf = SigMFFile()
385-
for idx in range(3):
386-
simulate_capture(sigf, idx, 1024)
390+
class TestBasicFunctionality(unittest.TestCase):
391+
"""test basic SigMFFile functionality"""
392+
393+
def test_default_constructor(self):
394+
"""test default constructor"""
395+
SigMFFile()
396+
397+
def test_set_non_required_global_field(self):
398+
"""test setting field not in schema"""
399+
meta = SigMFFile()
400+
meta.set_global_field("this_is:not_in_the_schema", None)
401+
402+
def test_add_annotation(self):
403+
"""test basic annotation addition"""
404+
meta = SigMFFile()
405+
meta.add_capture(start_index=0)
406+
annot = {"latitude": 40.0, "longitude": -105.0}
407+
meta.add_annotation(start_index=0, length=128, metadata=annot)
408+
409+
def test_load_from_archive(self):
410+
"""test loading from archive"""
411+
with tempfile.NamedTemporaryFile(suffix=".sigmf") as temp_file:
412+
# create temporary data file
413+
with tempfile.NamedTemporaryFile(suffix=".sigmf-data", delete=False) as data_file:
414+
TEST_FLOAT32_DATA.tofile(data_file.name)
415+
meta = SigMFFile(TEST_METADATA, data_file=data_file.name)
416+
archive_path = meta.archive(name=temp_file.name, overwrite=True)
417+
loopback = sigmf.fromarchive(archive_path=archive_path)
418+
self.assertEqual(loopback._metadata, meta._metadata)
387419

388420

389421
class TestOverwrite(unittest.TestCase):

0 commit comments

Comments
 (0)