Lin_Simulator/cpp/tests/test_ldf_parser.cpp
Mohamed Salem cb60c2ad5d Steps 2-7: LDF loading, signal editing, Rx display, connection, BabyLIN backend, scheduler
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>
2026-04-04 14:21:24 +02:00

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"