/** * 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 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 #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"