""" test_ldf_loading.py — Tests for LDF loading integration with the GUI. Tests that the MainWindow correctly populates its tables and widgets when an LDF file is loaded. This is the GUI integration layer — ldf_handler parsing is tested separately in test_ldf_handler.py. """ import sys import pytest from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt SAMPLE_LDF = str(Path(__file__).parent.parent.parent / "resources" / "sample.ldf") @pytest.fixture(scope="session") def app(): application = QApplication.instance() or QApplication(sys.argv) yield application @pytest.fixture def window(app): from main_window import MainWindow w = MainWindow() yield w w.close() @pytest.fixture def loaded_window(window): """A MainWindow with the sample LDF already loaded.""" window._load_ldf_file(SAMPLE_LDF) return window class TestLdfLoading: """Test that loading an LDF updates the GUI correctly.""" def test_ldf_path_shown(self, loaded_window): assert SAMPLE_LDF in loaded_window.ldf_path_edit.text() def test_baud_rate_updated(self, loaded_window): assert "19200" in loaded_window.lbl_baud_rate.text() def test_ldf_data_stored(self, loaded_window): assert loaded_window._ldf_data is not None assert loaded_window._ldf_data.baudrate == 19200 class TestTxTablePopulation: """Test that Tx tree is filled with master frames and signals.""" def test_tx_frame_count(self, loaded_window): """Should have 2 master Tx frames as top-level items.""" assert loaded_window.tx_table.topLevelItemCount() == 2 def test_tx_frame_names(self, loaded_window): names = [] for i in range(loaded_window.tx_table.topLevelItemCount()): names.append(loaded_window.tx_table.topLevelItem(i).text(0)) assert "Motor_Command" in names assert "Door_Command" in names def test_tx_frame_ids(self, loaded_window): ids = [] for i in range(loaded_window.tx_table.topLevelItemCount()): ids.append(loaded_window.tx_table.topLevelItem(i).text(1)) assert "0x10" in ids assert "0x11" in ids def test_tx_frame_lengths(self, loaded_window): for i in range(loaded_window.tx_table.topLevelItemCount()): length = loaded_window.tx_table.topLevelItem(i).text(2) assert length == "2" def test_tx_value_column_shows_bytes(self, loaded_window): """Value column (col 4) should show frame bytes in hex mode (default).""" for i in range(loaded_window.tx_table.topLevelItemCount()): val = loaded_window.tx_table.topLevelItem(i).text(4) assert val == "00 00" # 2 zero bytes in hex def test_tx_signals_as_children(self, loaded_window): """Each frame should have signal children that can be expanded.""" # Motor_Command has 3 signals: MotorEnable, MotorDirection, MotorSpeed item = loaded_window.tx_table.topLevelItem(0) assert item.childCount() >= 2 # At least 2 signals per frame def test_tx_signal_names(self, loaded_window): """Signal children should show signal names.""" item = loaded_window.tx_table.topLevelItem(0) sig_names = [item.child(j).text(0).strip() for j in range(item.childCount())] # Check that at least one known signal is present all_names = " ".join(sig_names) assert "Motor" in all_names or "Door" in all_names def test_tx_interval_from_schedule(self, loaded_window): """Per-frame interval should be auto-filled from the first schedule table.""" intervals = [] for i in range(loaded_window.tx_table.topLevelItemCount()): intervals.append(loaded_window.tx_table.topLevelItem(i).text(3)) assert "10" in intervals class TestRxTablePopulation: """Test that Rx tree is prepared with slave frame info and signals.""" def test_rx_frame_count(self, loaded_window): """Should have 2 slave Rx frames as top-level items.""" assert loaded_window.rx_table.topLevelItemCount() == 2 def test_rx_frame_names(self, loaded_window): names = [] for i in range(loaded_window.rx_table.topLevelItemCount()): names.append(loaded_window.rx_table.topLevelItem(i).text(1)) assert "Motor_Status" in names assert "Door_Status" in names def test_rx_frame_ids(self, loaded_window): ids = [] for i in range(loaded_window.rx_table.topLevelItemCount()): ids.append(loaded_window.rx_table.topLevelItem(i).text(2)) assert "0x20" in ids assert "0x21" in ids def test_rx_timestamp_placeholder(self, loaded_window): """Timestamp should show placeholder until real data arrives.""" for i in range(loaded_window.rx_table.topLevelItemCount()): ts = loaded_window.rx_table.topLevelItem(i).text(0) assert ts == "—" def test_rx_signals_as_children(self, loaded_window): """Each Rx frame should have signal children.""" item = loaded_window.rx_table.topLevelItem(0) assert item.childCount() >= 1 class TestHexDecToggle: """Test the hex/dec display toggle.""" def test_hex_mode_default(self, loaded_window): """Default mode is hex.""" assert loaded_window.chk_hex_mode.isChecked() def test_signal_value_hex_format(self, loaded_window): """Signal values should display in hex when hex mode is on.""" item = loaded_window.tx_table.topLevelItem(0) sig = item.child(0) val = sig.text(4) assert val.startswith("0x") def test_signal_value_dec_format(self, loaded_window): """Switching to dec mode should show decimal values.""" loaded_window.chk_hex_mode.setChecked(False) item = loaded_window.tx_table.topLevelItem(0) sig = item.child(0) val = sig.text(4) assert not val.startswith("0x") # Restore hex mode loaded_window.chk_hex_mode.setChecked(True) def test_frame_value_hex_format(self, loaded_window): """Frame value should show hex bytes like '00 00'.""" item = loaded_window.tx_table.topLevelItem(0) val = item.text(4) # Hex format: each byte is 2 uppercase hex chars assert all(len(b) == 2 for b in val.split()) class TestScheduleCombo: """Test schedule table dropdown population.""" def test_schedule_count(self, loaded_window): assert loaded_window.combo_schedule.count() == 2 def test_schedule_names(self, loaded_window): items = [loaded_window.combo_schedule.itemText(i) for i in range(loaded_window.combo_schedule.count())] assert "NormalSchedule" in items assert "FastSchedule" in items class TestErrorHandling: """Test that invalid files are handled gracefully.""" def test_invalid_file_no_crash(self, window, tmp_path, monkeypatch): """Loading an invalid file should not crash — show error dialog.""" bad_file = tmp_path / "bad.ldf" bad_file.write_text("not valid") # Monkeypatch QMessageBox.critical to avoid a modal dialog blocking the test. # monkeypatch is a pytest fixture that temporarily replaces functions. from PyQt6.QtWidgets import QMessageBox monkeypatch.setattr(QMessageBox, "critical", lambda *args, **kwargs: None) # Should not raise window._load_ldf_file(str(bad_file)) # Tables should remain empty assert window.tx_table.topLevelItemCount() == 0 def test_reload_clears_previous(self, loaded_window): """Loading a new file should clear previous data.""" # First load happened in fixture — 2 Tx frames assert loaded_window.tx_table.topLevelItemCount() == 2 # Load the same file again — should still be 2 (not 4) loaded_window._load_ldf_file(SAMPLE_LDF) assert loaded_window.tx_table.topLevelItemCount() == 2 class TestAutoReload: """Test the file watcher setup.""" def test_file_watcher_active(self, loaded_window): """After loading, the file should be watched.""" watched = loaded_window._file_watcher.files() assert len(watched) > 0 assert SAMPLE_LDF in watched def test_auto_reload_checkbox_controls_reload(self, loaded_window): """When auto-reload is unchecked, file changes should not reload.""" loaded_window.chk_auto_reload.setChecked(False) # Simulate file change signal loaded_window._on_ldf_file_changed(SAMPLE_LDF) # Should still work (no crash), data stays the same assert loaded_window._ldf_data is not None