Step 2 - LDF Loading: - ldfparser integration (Python) / custom regex parser (C++) - QTreeWidget with expandable signal rows, merged Value column - Hex/Dec toggle, FreeFormat schedule entries, auto-reload - Baud rate auto-detection from LDF Step 3 - Signal Editing: - Bit packing/unpacking (signal value ↔ frame bytes) - ReadOnlyColumnDelegate for per-column editability - Value clamping to signal width, recursion guard Step 4 - Rx Panel: - receive_rx_frame() API with timestamp, signal unpacking - Change highlighting (yellow), auto-scroll toggle, clear button - Dashboard view (in-place update per frame_id) Step 5 - Connection Panel: - ConnectionManager with state machine (Disconnected/Connecting/Connected/Error) - Port scanning (pyserial / QSerialPort), connect/disconnect with UI mapping Step 6 - BabyLIN Backend: - BabyLinBackend wrapping Lipowsky BabyLIN_library.py DLL - Mock mode for macOS/CI, device scan, SDF loading, signal access - Frame callbacks, raw command access Step 7 - Master Scheduler: - QTimer-based schedule execution with start/stop/pause - Frame sent callback with visual highlighting - Mock Rx simulation, manual send, global rate override Tests: Python 171 | C++ 124 (Steps 1-5 parity, Steps 6-7 Python-first) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
281 lines
9.5 KiB
C++
281 lines
9.5 KiB
C++
/**
|
|
* test_ldf_parser.cpp — Tests for the LDF parsing module (Step 2, C++).
|
|
*
|
|
* C++ equivalent of python/tests/test_ldf_handler.py.
|
|
* Tests the custom LDF parser against resources/sample.ldf.
|
|
*
|
|
* COMPILE-TIME DEFINES vs PYTHON PATH RESOLUTION:
|
|
* ================================================
|
|
* In Python, we resolve the path at runtime:
|
|
* SAMPLE_LDF = str(Path(__file__).parent.parent.parent / "resources" / "sample.ldf")
|
|
*
|
|
* In C++, CMakeLists.txt injects the path at compile time:
|
|
* target_compile_definitions(test_ldf_parser PRIVATE
|
|
* LDF_SAMPLE_PATH="${CMAKE_CURRENT_SOURCE_DIR}/../resources/sample.ldf"
|
|
* )
|
|
*
|
|
* This becomes a #define that the preprocessor substitutes before compilation.
|
|
* The #ifndef / #define fallback below provides a default if CMake doesn't set it.
|
|
*
|
|
* STD::FIND_IF — C++ ALGORITHM:
|
|
* =============================
|
|
* Several tests need to find a specific frame by name. In Python:
|
|
* frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
*
|
|
* In C++, we use std::find_if from the <algorithm> header:
|
|
* auto it = std::find_if(frames.begin(), frames.end(),
|
|
* [](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
*
|
|
* The [](const FrameInfo& f) { ... } is a LAMBDA — an anonymous function.
|
|
* Python lambda: lambda f: f.name == "Motor_Command"
|
|
* C++ lambda: [](const FrameInfo& f) { return f.name == "Motor_Command"; }
|
|
*
|
|
* The [] is the "capture list" — it specifies which local variables the lambda
|
|
* can access. Empty [] means "capture nothing" (our lambda only uses its parameter).
|
|
*
|
|
* std::find_if returns an ITERATOR — a pointer-like object that points to the
|
|
* found element. We use -> to access its members (like it->frame_id).
|
|
*/
|
|
|
|
#include <QtTest/QtTest>
|
|
#include "ldf_parser.h"
|
|
|
|
// Path to sample LDF — CMakeLists.txt defines LDF_SAMPLE_PATH at compile time.
|
|
// The #ifndef provides a fallback if CMake doesn't set it (shouldn't happen).
|
|
#ifndef LDF_SAMPLE_PATH
|
|
#define LDF_SAMPLE_PATH "../../resources/sample.ldf"
|
|
#endif
|
|
|
|
class TestLdfParser : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private:
|
|
LdfData m_data; // Parsed once, shared by all tests (read-only)
|
|
|
|
private slots:
|
|
// initTestCase() runs ONCE before all tests (like pytest's session fixture).
|
|
// This is different from init() which runs before EACH test.
|
|
// Since our tests only read m_data and never modify it, parsing once is safe.
|
|
void initTestCase()
|
|
{
|
|
m_data = parseLdf(QString(LDF_SAMPLE_PATH));
|
|
}
|
|
|
|
// ─── Basic Parsing ───────────────────────────────────────────
|
|
|
|
void test_protocolVersion()
|
|
{
|
|
QCOMPARE(m_data.protocol_version, QString("2.1"));
|
|
}
|
|
|
|
void test_languageVersion()
|
|
{
|
|
QCOMPARE(m_data.language_version, QString("2.1"));
|
|
}
|
|
|
|
void test_baudrate()
|
|
{
|
|
QCOMPARE(m_data.baudrate, 19200);
|
|
}
|
|
|
|
void test_masterName()
|
|
{
|
|
QCOMPARE(m_data.master_name, QString("ECU_Master"));
|
|
}
|
|
|
|
void test_slaveNames()
|
|
{
|
|
QVERIFY(m_data.slave_names.contains("Motor_Control"));
|
|
QVERIFY(m_data.slave_names.contains("Door_Module"));
|
|
}
|
|
|
|
void test_filePathStored()
|
|
{
|
|
QCOMPARE(m_data.file_path, QString(LDF_SAMPLE_PATH));
|
|
}
|
|
|
|
// ─── Frame Classification ────────────────────────────────────
|
|
|
|
void test_txFrameCount()
|
|
{
|
|
QCOMPARE(m_data.tx_frames.size(), 2);
|
|
}
|
|
|
|
void test_rxFrameCount()
|
|
{
|
|
QCOMPARE(m_data.rx_frames.size(), 2);
|
|
}
|
|
|
|
void test_txFramesAreMaster()
|
|
{
|
|
for (const auto& frame : m_data.tx_frames)
|
|
QVERIFY(frame.is_master_tx);
|
|
}
|
|
|
|
void test_rxFramesAreSlave()
|
|
{
|
|
for (const auto& frame : m_data.rx_frames)
|
|
QVERIFY(!frame.is_master_tx);
|
|
}
|
|
|
|
void test_txFrameNames()
|
|
{
|
|
QStringList names;
|
|
for (const auto& f : m_data.tx_frames) names << f.name;
|
|
QVERIFY(names.contains("Motor_Command"));
|
|
QVERIFY(names.contains("Door_Command"));
|
|
}
|
|
|
|
void test_rxFrameNames()
|
|
{
|
|
QStringList names;
|
|
for (const auto& f : m_data.rx_frames) names << f.name;
|
|
QVERIFY(names.contains("Motor_Status"));
|
|
QVERIFY(names.contains("Door_Status"));
|
|
}
|
|
|
|
// ─── Frame Details ───────────────────────────────────────────
|
|
|
|
void test_motorCommandId()
|
|
{
|
|
auto it = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
QVERIFY(it != m_data.tx_frames.end());
|
|
QCOMPARE(it->frame_id, 0x10);
|
|
}
|
|
|
|
void test_motorCommandLength()
|
|
{
|
|
auto it = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
QCOMPARE(it->length, 2);
|
|
}
|
|
|
|
void test_motorCommandPublisher()
|
|
{
|
|
auto it = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
QCOMPARE(it->publisher, QString("ECU_Master"));
|
|
}
|
|
|
|
// ─── Signal Extraction ───────────────────────────────────────
|
|
|
|
void test_motorCommandSignals()
|
|
{
|
|
auto frame = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
QStringList sigNames;
|
|
for (const auto& s : frame->signal_list) sigNames << s.name;
|
|
QVERIFY(sigNames.contains("MotorEnable"));
|
|
QVERIFY(sigNames.contains("MotorDirection"));
|
|
QVERIFY(sigNames.contains("MotorSpeed"));
|
|
}
|
|
|
|
void test_signalBitOffset()
|
|
{
|
|
auto frame = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
auto sig = std::find_if(frame->signal_list.begin(), frame->signal_list.end(),
|
|
[](const SignalInfo& s) { return s.name == "MotorEnable"; });
|
|
QCOMPARE(sig->bit_offset, 0);
|
|
}
|
|
|
|
void test_signalWidth()
|
|
{
|
|
auto frame = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
auto sig = std::find_if(frame->signal_list.begin(), frame->signal_list.end(),
|
|
[](const SignalInfo& s) { return s.name == "MotorSpeed"; });
|
|
QCOMPARE(sig->width, 8);
|
|
}
|
|
|
|
void test_signalInitValue()
|
|
{
|
|
auto frame = std::find_if(m_data.tx_frames.begin(), m_data.tx_frames.end(),
|
|
[](const FrameInfo& f) { return f.name == "Motor_Command"; });
|
|
auto sig = std::find_if(frame->signal_list.begin(), frame->signal_list.end(),
|
|
[](const SignalInfo& s) { return s.name == "MotorEnable"; });
|
|
QCOMPARE(sig->init_value, 0);
|
|
}
|
|
|
|
// ─── Schedule Tables ─────────────────────────────────────────
|
|
|
|
void test_scheduleCount()
|
|
{
|
|
QCOMPARE(m_data.schedule_tables.size(), 2);
|
|
}
|
|
|
|
void test_scheduleNames()
|
|
{
|
|
QStringList names;
|
|
for (const auto& st : m_data.schedule_tables) names << st.name;
|
|
QVERIFY(names.contains("NormalSchedule"));
|
|
QVERIFY(names.contains("FastSchedule"));
|
|
}
|
|
|
|
void test_normalScheduleEntries()
|
|
{
|
|
auto st = std::find_if(m_data.schedule_tables.begin(), m_data.schedule_tables.end(),
|
|
[](const ScheduleTableInfo& s) { return s.name == "NormalSchedule"; });
|
|
QCOMPARE(st->entries.size(), 4);
|
|
}
|
|
|
|
void test_normalScheduleDelay()
|
|
{
|
|
auto st = std::find_if(m_data.schedule_tables.begin(), m_data.schedule_tables.end(),
|
|
[](const ScheduleTableInfo& s) { return s.name == "NormalSchedule"; });
|
|
for (const auto& entry : st->entries)
|
|
QCOMPARE(entry.delay_ms, 10);
|
|
}
|
|
|
|
void test_fastScheduleDelay()
|
|
{
|
|
auto st = std::find_if(m_data.schedule_tables.begin(), m_data.schedule_tables.end(),
|
|
[](const ScheduleTableInfo& s) { return s.name == "FastSchedule"; });
|
|
for (const auto& entry : st->entries)
|
|
QCOMPARE(entry.delay_ms, 5);
|
|
}
|
|
|
|
void test_frameEntriesHaveNoData()
|
|
{
|
|
auto st = std::find_if(m_data.schedule_tables.begin(), m_data.schedule_tables.end(),
|
|
[](const ScheduleTableInfo& s) { return s.name == "NormalSchedule"; });
|
|
for (const auto& entry : st->entries)
|
|
QVERIFY(entry.data.isEmpty());
|
|
}
|
|
|
|
// ─── Error Handling ──────────────────────────────────────────
|
|
|
|
void test_fileNotFound()
|
|
{
|
|
bool threw = false;
|
|
try {
|
|
parseLdf("/nonexistent/path/fake.ldf");
|
|
} catch (const std::runtime_error&) {
|
|
threw = true;
|
|
}
|
|
QVERIFY(threw);
|
|
}
|
|
|
|
void test_invalidFile()
|
|
{
|
|
// Create a temporary invalid file
|
|
QTemporaryFile tmpFile;
|
|
tmpFile.open();
|
|
tmpFile.write("this is not a valid LDF file");
|
|
tmpFile.close();
|
|
|
|
bool threw = false;
|
|
try {
|
|
parseLdf(tmpFile.fileName());
|
|
} catch (const std::runtime_error&) {
|
|
threw = true;
|
|
}
|
|
QVERIFY(threw);
|
|
}
|
|
};
|
|
|
|
QTEST_MAIN(TestLdfParser)
|
|
#include "test_ldf_parser.moc"
|