Step 7 - Master Scheduler (Python + C++): - QTimer-based schedule execution with start/stop/pause/resume - Frame sent callback with light-blue visual highlighting - Mock Rx simulation with incrementing counter data - Manual send button for individual frame injection - Global rate spinbox with live update during run - Schedule table switching Step 8 - Integration (Python): - BabyLinBackend wired into MainWindow - Global rate spinbox live-updates scheduler - End-to-end tests: load → edit signals → run → Rx arrives → stop - Edge case tests: no LDF, no selection, double stop - Full workflow verified with 182 Python tests Tests: Python 182 | C++ 124 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
203 lines
6.2 KiB
Python
203 lines
6.2 KiB
Python
"""
|
|
test_integration.py — Step 8: End-to-end integration tests.
|
|
|
|
Tests the full workflow:
|
|
Load LDF → edit signals → start scheduler → see Rx → stop
|
|
Verifies all components work together correctly.
|
|
"""
|
|
|
|
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
|
|
from PyQt6.QtTest import QTest
|
|
|
|
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()
|
|
return w
|
|
|
|
|
|
class TestFullWorkflow:
|
|
"""Test the complete user workflow from start to finish."""
|
|
|
|
def test_load_edit_run_stop(self, window):
|
|
"""
|
|
Complete flow:
|
|
1. Load LDF
|
|
2. Edit a signal value
|
|
3. Start scheduler
|
|
4. Verify Rx data arrives
|
|
5. Stop scheduler
|
|
"""
|
|
# 1. Load LDF
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
assert window._ldf_data is not None
|
|
assert window.tx_table.topLevelItemCount() == 2
|
|
assert window.rx_table.topLevelItemCount() == 2
|
|
|
|
# 2. Edit a signal — set MotorSpeed to 200
|
|
frame_item = window.tx_table.topLevelItem(0) # Motor_Command
|
|
speed_sig = frame_item.child(2) # MotorSpeed
|
|
speed_sig.setText(4, "200")
|
|
|
|
# Verify frame bytes updated
|
|
frame_data = frame_item.data(0, Qt.ItemDataRole.UserRole)
|
|
assert frame_data['bytes'][1] == 200
|
|
|
|
# 3. Start scheduler
|
|
window.combo_schedule.setCurrentIndex(0)
|
|
window._on_start_scheduler()
|
|
assert window._scheduler.is_running
|
|
|
|
# 4. Wait for Rx data
|
|
QTest.qWait(100)
|
|
|
|
# Verify Rx timestamps appeared
|
|
has_rx = False
|
|
for i in range(window.rx_table.topLevelItemCount()):
|
|
if window.rx_table.topLevelItem(i).text(0) != "—":
|
|
has_rx = True
|
|
break
|
|
assert has_rx, "Should have received mock Rx data"
|
|
|
|
# 5. Stop
|
|
window._on_stop_scheduler()
|
|
assert not window._scheduler.is_running
|
|
|
|
def test_hex_dec_toggle_during_run(self, window):
|
|
"""Toggle hex/dec while scheduler is running."""
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window.combo_schedule.setCurrentIndex(0)
|
|
window._on_start_scheduler()
|
|
QTest.qWait(50)
|
|
|
|
# Toggle to decimal
|
|
window.chk_hex_mode.setChecked(False)
|
|
QTest.qWait(50)
|
|
|
|
# Toggle back to hex
|
|
window.chk_hex_mode.setChecked(True)
|
|
QTest.qWait(50)
|
|
|
|
window._on_stop_scheduler()
|
|
# No crash = pass
|
|
|
|
def test_pause_resume_during_run(self, window):
|
|
"""Pause and resume the scheduler."""
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window.combo_schedule.setCurrentIndex(0)
|
|
window._on_start_scheduler()
|
|
QTest.qWait(50)
|
|
|
|
# Pause
|
|
window._on_pause_scheduler()
|
|
assert window._scheduler.is_paused
|
|
QTest.qWait(50)
|
|
|
|
# Resume
|
|
window._on_pause_scheduler()
|
|
assert not window._scheduler.is_paused
|
|
QTest.qWait(50)
|
|
|
|
window._on_stop_scheduler()
|
|
|
|
def test_clear_rx_during_run(self, window):
|
|
"""Clear Rx data while scheduler is running."""
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window.combo_schedule.setCurrentIndex(0)
|
|
window._on_start_scheduler()
|
|
QTest.qWait(100)
|
|
|
|
window._on_clear_rx()
|
|
|
|
# All timestamps should be reset
|
|
for i in range(window.rx_table.topLevelItemCount()):
|
|
assert window.rx_table.topLevelItem(i).text(0) == "—"
|
|
|
|
# But scheduler should still be running
|
|
assert window._scheduler.is_running
|
|
window._on_stop_scheduler()
|
|
|
|
def test_change_global_rate_live(self, window):
|
|
"""Change global rate while scheduler is running."""
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window.combo_schedule.setCurrentIndex(0)
|
|
window._on_start_scheduler()
|
|
QTest.qWait(50)
|
|
|
|
window.spin_global_rate.setValue(10)
|
|
QTest.qWait(50)
|
|
|
|
window.spin_global_rate.setValue(100)
|
|
QTest.qWait(50)
|
|
|
|
window._on_stop_scheduler()
|
|
|
|
def test_reload_ldf_stops_scheduler(self, window):
|
|
"""Reloading LDF while scheduler runs should stop first."""
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window.combo_schedule.setCurrentIndex(0)
|
|
window._on_start_scheduler()
|
|
QTest.qWait(50)
|
|
|
|
# Reload — scheduler should handle this gracefully
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
|
|
# Tables should still be populated
|
|
assert window.tx_table.topLevelItemCount() == 2
|
|
|
|
|
|
class TestBackendIntegration:
|
|
"""Test BabyLinBackend integration (mock mode)."""
|
|
|
|
def test_backend_exists(self, window):
|
|
assert window._backend is not None
|
|
assert window._backend.is_mock_mode
|
|
|
|
def test_backend_scan_from_gui(self, window):
|
|
"""Backend scan should return mock device."""
|
|
devices = window._backend.scan_devices()
|
|
assert len(devices) >= 1
|
|
|
|
|
|
class TestEdgeCase:
|
|
"""Test edge cases and error conditions."""
|
|
|
|
def test_start_without_ldf(self, window, monkeypatch):
|
|
"""Starting scheduler without LDF loaded should not crash."""
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
monkeypatch.setattr(QMessageBox, "warning", lambda *args: None)
|
|
window._on_start_scheduler()
|
|
assert not window._scheduler.is_running
|
|
|
|
def test_manual_send_without_selection(self, window, monkeypatch):
|
|
"""Manual send without selecting a frame should warn."""
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
warned = []
|
|
monkeypatch.setattr(QMessageBox, "warning",
|
|
lambda *args: warned.append(True))
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window._on_manual_send()
|
|
assert len(warned) > 0
|
|
|
|
def test_double_stop(self, window):
|
|
"""Stopping when already stopped should not crash."""
|
|
window._load_ldf_file(SAMPLE_LDF)
|
|
window._on_stop_scheduler()
|
|
window._on_stop_scheduler() # No crash
|