from dataclasses import replace from pathlib import Path import struct import tempfile import unittest from unittest.mock import MagicMock, patch from logic_analyzer import saleae from logic_analyzer.files import ImportCancelled def binary(path, edges=(), initial=0, start=0., end=1., version=0): Path(path).write_bytes(struct.pack('<8siiIddQ', b'', version, 0, initial, start, end, len(edges)) + struct.pack('<%dd' % len(edges), *edges)) class SaleaeCoreTests(unittest.TestCase): def test_constant_channel_tail_and_sparse_channel_numbers(self): with tempfile.TemporaryDirectory() as directory: binary(Path(directory) / 'digital_2.bin', [.2, .3]) binary(Path(directory) / 'digital_7.bin', initial=1) capture = saleae.read_binary(directory, (2, 7)) self.assertEqual([c.name for c in capture.channels], ['D2', 'D7']) self.assertEqual(capture.end, 1.) self.assertEqual(capture.channels[0].level_at(.25), 1) self.assertEqual(capture.channels[0].level_at(.9), 0) self.assertEqual(capture.channels[1].level_at(.9), 1) def test_reject_bad_binary_and_cancel(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / 'digital_0.bin' for kwargs in ({'version': 1}, {'edges': [.4, .3]}, {'edges': [float('nan')]}, {'end': -1}, {'initial': 2}): binary(path, **kwargs) with self.assertRaises(ValueError): saleae.read_binary(directory, (0,)) binary(path, [.2]) with self.assertRaises(ImportCancelled): saleae.read_binary(directory, (0,), lambda: True) def test_settings_validation(self): good = saleae.SaleaeSettings('id') good.validate() for kwargs in ({'channels': ()}, {'channels': (1, 1)}, {'channels': (16,)}, {'sample_rate': 0}, {'duration': float('nan')}, {'port': 0}, {'buffer_mb': 1}, {'threshold': 5}, {'device_id': ''}): with self.assertRaises(ValueError): replace(good, **kwargs).validate() def fake_api(self): api = MagicMock() manager = api.Manager.connect.return_value.__enter__.return_value capture = manager.start_capture.return_value def export(directory, **kwargs): for channel in kwargs['digital_channels']: binary(Path(directory) / ('digital_%d.bin' % channel), [.25]) capture.export_raw_data_binary.side_effect = export return api, manager, capture def test_stop_exports_and_closes_exactly_once(self): api, manager, capture = self.fake_api() settings = saleae.SaleaeSettings('abc', channels=(3, 5), threshold=1.8) with patch.object(saleae, 'automation_api', return_value=api): result = saleae.acquire(settings, lambda: True, lambda: False) self.assertEqual(result.sample_rate, 24000000) self.assertIn('abc', result.source) self.assertEqual([c.name for c in result.channels], ['D3', 'D5']) capture.stop.assert_called_once() capture.wait.assert_not_called() capture.close.assert_called_once() api.LogicDeviceConfiguration.assert_called_once_with( enabled_digital_channels=[3, 5], digital_sample_rate=24000000, digital_threshold_volts=1.8) self.assertFalse(Path(capture.export_raw_data_binary.call_args[0][0]).exists()) def test_cancel_stops_without_export_and_failure_closes(self): api, manager, capture = self.fake_api() with patch.object(saleae, 'automation_api', return_value=api): calls = iter((False, True, True)) with self.assertRaises(ImportCancelled): saleae.acquire(saleae.SaleaeSettings('id'), lambda: False, lambda: next(calls)) capture.stop.assert_called_once() capture.close.assert_called_once() capture.export_raw_data_binary.assert_not_called() capture.reset_mock() capture.stop.side_effect = RuntimeError('USB disconnected') with patch.object(saleae, 'automation_api', return_value=api): with self.assertRaisesRegex(RuntimeError, 'USB'): saleae.acquire(saleae.SaleaeSettings('id'), lambda: True, lambda: False) capture.stop.assert_called_once() capture.close.assert_called_once()