Steps 7-8: Master scheduler and end-to-end integration
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>
This commit is contained in:
parent
cb60c2ad5d
commit
251f5d327e
4
PLAN.md
4
PLAN.md
@ -165,7 +165,7 @@ LIN_Control_Tool/
|
||||
---
|
||||
|
||||
### Step 7 — Master Scheduler
|
||||
- **Status:** Python DONE (171 tests) | C++ Not started
|
||||
- **Status:** DONE — Python (171 tests) | C++ (124 tests + scheduler)
|
||||
- **Features:** QTimer-based schedule execution, start/stop/pause, frame sent callback with visual highlighting, mock Rx simulation, manual send, global rate override, schedule table switching
|
||||
- **Goal:** Periodic frame transmission using LDF schedule tables.
|
||||
|
||||
@ -190,7 +190,7 @@ LIN_Control_Tool/
|
||||
---
|
||||
|
||||
### Step 8 — Integration & End-to-End
|
||||
- **Status:** Not started
|
||||
- **Status:** In progress
|
||||
- **Goal:** Wire all components together, full workflow testing.
|
||||
|
||||
**Features:**
|
||||
|
||||
@ -61,6 +61,8 @@ qt_add_executable(lin_simulator
|
||||
src/ldf_parser.h
|
||||
src/connection_manager.cpp
|
||||
src/connection_manager.h
|
||||
src/scheduler.cpp
|
||||
src/scheduler.h
|
||||
)
|
||||
|
||||
target_link_libraries(lin_simulator PRIVATE Qt6::Widgets Qt6::SerialPort)
|
||||
@ -84,6 +86,8 @@ qt_add_executable(test_main_window
|
||||
src/ldf_parser.h
|
||||
src/connection_manager.cpp
|
||||
src/connection_manager.h
|
||||
src/scheduler.cpp
|
||||
src/scheduler.h
|
||||
)
|
||||
|
||||
target_link_libraries(test_main_window PRIVATE Qt6::Widgets Qt6::SerialPort Qt6::Test)
|
||||
@ -144,6 +148,8 @@ qt_add_executable(test_ldf_loading
|
||||
src/ldf_parser.h
|
||||
src/connection_manager.cpp
|
||||
src/connection_manager.h
|
||||
src/scheduler.cpp
|
||||
src/scheduler.h
|
||||
)
|
||||
|
||||
target_link_libraries(test_ldf_loading PRIVATE Qt6::Widgets Qt6::SerialPort Qt6::Test)
|
||||
@ -162,6 +168,8 @@ qt_add_executable(test_signal_editing
|
||||
src/ldf_parser.h
|
||||
src/connection_manager.cpp
|
||||
src/connection_manager.h
|
||||
src/scheduler.cpp
|
||||
src/scheduler.h
|
||||
)
|
||||
|
||||
target_link_libraries(test_signal_editing PRIVATE Qt6::Widgets Qt6::SerialPort Qt6::Test)
|
||||
@ -180,6 +188,8 @@ qt_add_executable(test_rx_realtime
|
||||
src/ldf_parser.h
|
||||
src/connection_manager.cpp
|
||||
src/connection_manager.h
|
||||
src/scheduler.cpp
|
||||
src/scheduler.h
|
||||
)
|
||||
|
||||
target_link_libraries(test_rx_realtime PRIVATE Qt6::Widgets Qt6::SerialPort Qt6::Test)
|
||||
|
||||
@ -155,6 +155,13 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
connect(m_fileWatcher, &QFileSystemWatcher::fileChanged,
|
||||
this, &MainWindow::onLdfFileChanged);
|
||||
|
||||
// Scheduler — cycles through schedule table entries
|
||||
m_scheduler = new Scheduler(this);
|
||||
m_scheduler->setFrameSentCallback(
|
||||
[this](const QString& name, int fid, bool isTx) { onFrameSent(name, fid, isTx); });
|
||||
m_scheduler->setRxDataCallback(
|
||||
[this](int fid, const QVector<int>& data) { receiveRxFrame(fid, data); });
|
||||
|
||||
createMenuBar();
|
||||
createLdfToolbar();
|
||||
createCentralWidget();
|
||||
@ -414,8 +421,11 @@ void MainWindow::createControlBar()
|
||||
toolbar->addSeparator();
|
||||
|
||||
m_btnStart = new QPushButton(tr("▶ Start"));
|
||||
connect(m_btnStart, &QPushButton::clicked, this, &MainWindow::onStartScheduler);
|
||||
m_btnStop = new QPushButton(tr("■ Stop"));
|
||||
connect(m_btnStop, &QPushButton::clicked, this, &MainWindow::onStopScheduler);
|
||||
m_btnPause = new QPushButton(tr("⏸ Pause"));
|
||||
connect(m_btnPause, &QPushButton::clicked, this, &MainWindow::onPauseScheduler);
|
||||
m_btnStart->setEnabled(false);
|
||||
m_btnStop->setEnabled(false);
|
||||
m_btnPause->setEnabled(false);
|
||||
@ -426,6 +436,7 @@ void MainWindow::createControlBar()
|
||||
toolbar->addSeparator();
|
||||
|
||||
m_btnManualSend = new QPushButton(tr("Send Selected Frame"));
|
||||
connect(m_btnManualSend, &QPushButton::clicked, this, &MainWindow::onManualSend);
|
||||
m_btnManualSend->setEnabled(false);
|
||||
toolbar->addWidget(m_btnManualSend);
|
||||
}
|
||||
@ -491,6 +502,10 @@ void MainWindow::loadLdfFile(const QString& filePath)
|
||||
populateScheduleCombo(*m_ldfData);
|
||||
setupFileWatcher(filePath);
|
||||
|
||||
// Enable scheduler controls
|
||||
m_btnStart->setEnabled(true);
|
||||
m_btnManualSend->setEnabled(true);
|
||||
|
||||
statusBar()->showMessage(
|
||||
tr("LDF loaded: %1 | %2 Tx, %3 Rx frames | %4 baud")
|
||||
.arg(m_ldfData->master_name)
|
||||
@ -1140,6 +1155,96 @@ void MainWindow::updateConnectionUi()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Scheduler Controls (Step 7) ──────────────────────────────────────
|
||||
|
||||
void MainWindow::onStartScheduler()
|
||||
{
|
||||
if (!m_ldfData)
|
||||
return;
|
||||
|
||||
int idx = m_comboSchedule->currentIndex();
|
||||
if (idx < 0 || idx >= m_ldfData->schedule_tables.size()) {
|
||||
QMessageBox::warning(this, tr("No Schedule"), tr("Please select a schedule table."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& schedule = m_ldfData->schedule_tables[idx];
|
||||
m_scheduler->setSchedule(schedule.entries, *m_ldfData);
|
||||
m_scheduler->setGlobalRate(m_spinGlobalRate->value());
|
||||
m_scheduler->start();
|
||||
|
||||
m_btnStart->setEnabled(false);
|
||||
m_btnStop->setEnabled(true);
|
||||
m_btnPause->setEnabled(true);
|
||||
m_btnPause->setText(tr("⏸ Pause"));
|
||||
|
||||
statusBar()->showMessage(tr("Scheduler started: %1").arg(schedule.name), 3000);
|
||||
}
|
||||
|
||||
void MainWindow::onStopScheduler()
|
||||
{
|
||||
m_scheduler->stop();
|
||||
m_btnStart->setEnabled(true);
|
||||
m_btnStop->setEnabled(false);
|
||||
m_btnPause->setEnabled(false);
|
||||
m_btnPause->setText(tr("⏸ Pause"));
|
||||
clearFrameHighlight();
|
||||
statusBar()->showMessage(tr("Scheduler stopped"), 3000);
|
||||
}
|
||||
|
||||
void MainWindow::onPauseScheduler()
|
||||
{
|
||||
if (m_scheduler->isPaused()) {
|
||||
m_scheduler->resume();
|
||||
m_btnPause->setText(tr("⏸ Pause"));
|
||||
statusBar()->showMessage(tr("Scheduler resumed"), 2000);
|
||||
} else {
|
||||
m_scheduler->pause();
|
||||
m_btnPause->setText(tr("▶ Resume"));
|
||||
statusBar()->showMessage(tr("Scheduler paused"), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onManualSend()
|
||||
{
|
||||
auto* item = m_txTable->currentItem();
|
||||
if (!item) {
|
||||
QMessageBox::warning(this, tr("No Frame Selected"), tr("Select a Tx frame to send."));
|
||||
return;
|
||||
}
|
||||
if (item->parent())
|
||||
item = item->parent();
|
||||
|
||||
QString frameName = item->text(0);
|
||||
m_scheduler->sendFrameNow(frameName);
|
||||
statusBar()->showMessage(tr("Sent: %1").arg(frameName), 2000);
|
||||
}
|
||||
|
||||
void MainWindow::onFrameSent(const QString& name, int /*frameId*/, bool isTx)
|
||||
{
|
||||
clearFrameHighlight();
|
||||
auto* tree = isTx ? m_txTable : m_rxTable;
|
||||
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
|
||||
auto* item = tree->topLevelItem(i);
|
||||
if (item->text(0) == name) {
|
||||
for (int col = 0; col < tree->columnCount(); ++col)
|
||||
item->setBackground(col, QBrush(QColor(173, 216, 230)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::clearFrameHighlight()
|
||||
{
|
||||
for (auto* tree : {m_txTable, m_rxTable}) {
|
||||
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
|
||||
auto* item = tree->topLevelItem(i);
|
||||
for (int col = 0; col < tree->columnCount(); ++col)
|
||||
item->setBackground(col, QBrush());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onAbout()
|
||||
{
|
||||
QMessageBox::about(
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
#include <QMap> // QMap<K,V> is Qt's dictionary — like Python's dict
|
||||
#include "ldf_parser.h"
|
||||
#include "connection_manager.h"
|
||||
#include "scheduler.h"
|
||||
#include <optional> // std::optional<T> — a value that might not exist (like Python's Optional[T] / None)
|
||||
|
||||
// --- Forward declarations: tell the compiler "these classes exist" without
|
||||
@ -131,6 +132,10 @@ public slots:
|
||||
void onRefreshDevices();
|
||||
void onConnect();
|
||||
void onDisconnect();
|
||||
void onStartScheduler();
|
||||
void onStopScheduler();
|
||||
void onPauseScheduler();
|
||||
void onManualSend();
|
||||
|
||||
// Public accessor for tests
|
||||
ConnectionManager* connectionManager() { return &m_connMgr; }
|
||||
@ -164,6 +169,8 @@ private:
|
||||
void updateRxFrame(QTreeWidgetItem* frameItem, int frameIndex, const QVector<int>& dataBytes);
|
||||
void refreshRxFrame(QTreeWidgetItem* frameItem);
|
||||
void updateConnectionUi();
|
||||
void onFrameSent(const QString& name, int frameId, bool isTx);
|
||||
void clearFrameHighlight();
|
||||
|
||||
// ── Step 3: Signal ↔ Frame byte sync (private) ──
|
||||
void onSignalValueEdited(QTreeWidgetItem* sigItem, QTreeWidgetItem* frameItem);
|
||||
@ -244,8 +251,8 @@ private:
|
||||
// Guard flag to prevent infinite recursion during signal↔frame sync
|
||||
bool m_updatingValues = false;
|
||||
|
||||
// Connection manager — handles serial port discovery and state
|
||||
ConnectionManager m_connMgr;
|
||||
Scheduler* m_scheduler;
|
||||
|
||||
// Track last known Rx signal values for change highlighting.
|
||||
// QMap<int, QMap<QString, int>> is a nested dictionary — equivalent to
|
||||
|
||||
165
cpp/src/scheduler.cpp
Normal file
165
cpp/src/scheduler.cpp
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* scheduler.cpp — LIN master scheduler implementation.
|
||||
* C++ equivalent of python/src/scheduler.py.
|
||||
*/
|
||||
|
||||
#include "scheduler.h"
|
||||
|
||||
Scheduler::Scheduler(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setSingleShot(true);
|
||||
connect(m_timer, &QTimer::timeout, this, &Scheduler::onTimerTick);
|
||||
}
|
||||
|
||||
void Scheduler::setSchedule(const QVector<ScheduleEntryInfo>& entries, const LdfData& ldfData)
|
||||
{
|
||||
m_entries = entries;
|
||||
m_ldfData = ldfData;
|
||||
m_hasLdfData = true;
|
||||
m_currentIndex = 0;
|
||||
}
|
||||
|
||||
void Scheduler::setGlobalRate(int rateMs)
|
||||
{
|
||||
m_globalRateMs = qMax(1, rateMs);
|
||||
}
|
||||
|
||||
void Scheduler::setFrameSentCallback(FrameSentCallback callback)
|
||||
{
|
||||
m_frameSentCb = callback;
|
||||
}
|
||||
|
||||
void Scheduler::setRxDataCallback(RxDataCallback callback)
|
||||
{
|
||||
m_rxDataCb = callback;
|
||||
}
|
||||
|
||||
void Scheduler::setTxDataProvider(TxDataProvider provider)
|
||||
{
|
||||
m_txDataProvider = provider;
|
||||
}
|
||||
|
||||
QString Scheduler::currentFrameName() const
|
||||
{
|
||||
if (!m_entries.isEmpty() && m_currentIndex >= 0 && m_currentIndex < m_entries.size())
|
||||
return m_entries[m_currentIndex].frame_name;
|
||||
return QString();
|
||||
}
|
||||
|
||||
void Scheduler::start()
|
||||
{
|
||||
if (m_entries.isEmpty())
|
||||
return;
|
||||
m_running = true;
|
||||
m_paused = false;
|
||||
m_currentIndex = 0;
|
||||
m_mockRxCounter = 0;
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
void Scheduler::stop()
|
||||
{
|
||||
m_timer->stop();
|
||||
m_running = false;
|
||||
m_paused = false;
|
||||
m_currentIndex = 0;
|
||||
}
|
||||
|
||||
void Scheduler::pause()
|
||||
{
|
||||
if (m_running && !m_paused) {
|
||||
m_timer->stop();
|
||||
m_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Scheduler::resume()
|
||||
{
|
||||
if (m_running && m_paused) {
|
||||
m_paused = false;
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
void Scheduler::sendFrameNow(const QString& frameName)
|
||||
{
|
||||
if (!m_hasLdfData)
|
||||
return;
|
||||
for (const auto& frame : m_ldfData.tx_frames) {
|
||||
if (frame.name == frameName) {
|
||||
sendTxFrame(frame.name, frame.frame_id, frame.length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Scheduler::scheduleNext()
|
||||
{
|
||||
if (!m_running || m_paused || m_entries.isEmpty())
|
||||
return;
|
||||
|
||||
const auto& entry = m_entries[m_currentIndex];
|
||||
int delayMs = entry.delay_ms > 0 ? entry.delay_ms : m_globalRateMs;
|
||||
m_timer->start(delayMs);
|
||||
}
|
||||
|
||||
void Scheduler::onTimerTick()
|
||||
{
|
||||
if (!m_running || m_paused)
|
||||
return;
|
||||
|
||||
const auto& entry = m_entries[m_currentIndex];
|
||||
|
||||
if (!entry.data.isEmpty()) {
|
||||
// FreeFormat entry
|
||||
if (m_frameSentCb)
|
||||
m_frameSentCb(entry.frame_name, 0, true);
|
||||
} else {
|
||||
processFrameEntry(entry.frame_name);
|
||||
}
|
||||
|
||||
// Advance (wrap around)
|
||||
m_currentIndex = (m_currentIndex + 1) % m_entries.size();
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
void Scheduler::processFrameEntry(const QString& frameName)
|
||||
{
|
||||
if (!m_hasLdfData)
|
||||
return;
|
||||
|
||||
for (const auto& frame : m_ldfData.tx_frames) {
|
||||
if (frame.name == frameName) {
|
||||
sendTxFrame(frame.name, frame.frame_id, frame.length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const auto& frame : m_ldfData.rx_frames) {
|
||||
if (frame.name == frameName) {
|
||||
receiveRxFrame(frame.name, frame.frame_id, frame.length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Scheduler::sendTxFrame(const QString& name, int frameId, int /*length*/)
|
||||
{
|
||||
if (m_frameSentCb)
|
||||
m_frameSentCb(name, frameId, true);
|
||||
}
|
||||
|
||||
void Scheduler::receiveRxFrame(const QString& name, int frameId, int length)
|
||||
{
|
||||
if (m_frameSentCb)
|
||||
m_frameSentCb(name, frameId, false);
|
||||
|
||||
if (m_rxDataCb) {
|
||||
m_mockRxCounter++;
|
||||
QVector<int> mockData;
|
||||
for (int i = 0; i < length; ++i)
|
||||
mockData.append((m_mockRxCounter + i) % 256);
|
||||
m_rxDataCb(frameId, mockData);
|
||||
}
|
||||
}
|
||||
70
cpp/src/scheduler.h
Normal file
70
cpp/src/scheduler.h
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* scheduler.h — LIN master scheduler for periodic frame transmission.
|
||||
* C++ equivalent of python/src/scheduler.py.
|
||||
*/
|
||||
|
||||
#ifndef SCHEDULER_H
|
||||
#define SCHEDULER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <functional>
|
||||
#include "ldf_parser.h"
|
||||
|
||||
// Callback types — std::function is C++'s equivalent of Python's Callable.
|
||||
// It can hold any callable: function pointer, lambda, method, etc.
|
||||
using FrameSentCallback = std::function<void(const QString& name, int frameId, bool isTx)>;
|
||||
using RxDataCallback = std::function<void(int frameId, const QVector<int>& data)>;
|
||||
using TxDataProvider = std::function<QVector<int>(int frameId)>;
|
||||
|
||||
class Scheduler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Scheduler(QObject* parent = nullptr);
|
||||
|
||||
// Configuration
|
||||
void setSchedule(const QVector<ScheduleEntryInfo>& entries, const LdfData& ldfData);
|
||||
void setGlobalRate(int rateMs);
|
||||
void setFrameSentCallback(FrameSentCallback callback);
|
||||
void setRxDataCallback(RxDataCallback callback);
|
||||
void setTxDataProvider(TxDataProvider provider);
|
||||
|
||||
// Properties
|
||||
bool isRunning() const { return m_running; }
|
||||
bool isPaused() const { return m_paused; }
|
||||
QString currentFrameName() const;
|
||||
|
||||
// Control
|
||||
void start();
|
||||
void stop();
|
||||
void pause();
|
||||
void resume();
|
||||
void sendFrameNow(const QString& frameName);
|
||||
|
||||
private slots:
|
||||
void onTimerTick();
|
||||
|
||||
private:
|
||||
void scheduleNext();
|
||||
void processFrameEntry(const QString& frameName);
|
||||
void sendTxFrame(const QString& name, int frameId, int length);
|
||||
void receiveRxFrame(const QString& name, int frameId, int length);
|
||||
|
||||
QVector<ScheduleEntryInfo> m_entries;
|
||||
LdfData m_ldfData;
|
||||
bool m_hasLdfData = false;
|
||||
int m_currentIndex = 0;
|
||||
bool m_running = false;
|
||||
bool m_paused = false;
|
||||
int m_globalRateMs = 50;
|
||||
int m_mockRxCounter = 0;
|
||||
|
||||
QTimer* m_timer;
|
||||
FrameSentCallback m_frameSentCb;
|
||||
RxDataCallback m_rxDataCb;
|
||||
TxDataProvider m_txDataProvider;
|
||||
};
|
||||
|
||||
#endif // SCHEDULER_H
|
||||
@ -210,6 +210,7 @@ from PyQt6.QtCore import QFileSystemWatcher
|
||||
from ldf_handler import parse_ldf, LdfData
|
||||
from connection_manager import ConnectionManager, ConnectionState
|
||||
from scheduler import Scheduler
|
||||
from babylin_backend import BabyLinBackend
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@ -268,6 +269,9 @@ class MainWindow(QMainWindow):
|
||||
self._scheduler.set_frame_sent_callback(self._on_frame_sent)
|
||||
self._scheduler.set_rx_data_callback(self.receive_rx_frame)
|
||||
|
||||
# BabyLIN backend — hardware communication (mock mode when no DLL)
|
||||
self._backend = BabyLinBackend()
|
||||
|
||||
# Build the UI in logical order
|
||||
self._create_menu_bar()
|
||||
self._create_ldf_toolbar()
|
||||
@ -662,6 +666,8 @@ class MainWindow(QMainWindow):
|
||||
"Per-frame intervals in the Tx table override this."
|
||||
)
|
||||
toolbar.addWidget(self.spin_global_rate)
|
||||
# Live-update scheduler when user changes the rate
|
||||
self.spin_global_rate.valueChanged.connect(self._on_global_rate_changed)
|
||||
|
||||
toolbar.addSeparator()
|
||||
|
||||
@ -1550,6 +1556,10 @@ class MainWindow(QMainWindow):
|
||||
self.btn_pause.setText("▶ Resume")
|
||||
self.statusBar().showMessage("Scheduler paused", 2000)
|
||||
|
||||
def _on_global_rate_changed(self, value: int):
|
||||
"""Live-update the scheduler's global rate when the spinbox changes."""
|
||||
self._scheduler.set_global_rate(value)
|
||||
|
||||
def _on_manual_send(self):
|
||||
"""Send the currently selected Tx frame manually."""
|
||||
item = self.tx_table.currentItem()
|
||||
|
||||
202
python/tests/test_integration.py
Normal file
202
python/tests/test_integration.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
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
|
||||
Loading…
x
Reference in New Issue
Block a user