From 9fbab921065577e6743b345469bfb2eee742d1f7 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 09:19:04 +0200 Subject: [PATCH] Q: GEM300 end-to-end conformance scenario harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single integration test that drives every top-level FSM through the coordinated lifecycle a real fab acceptance test exercises: 1. EPT NonScheduledTime -> Standby -> Productive (E116) 2. E84 load handshake at LP1 (CS_0 -> VALID -> L_REQ -> BUSY -> COMPT) 3. LoadPort transfer Loading -> InService; Carrier created + associated 4. CIDS NotConfirmed -> Confirmed via host ProceedWithCarrier (E87) 5. Slot map populated + CSMS NotRead -> Read 6. Two substrates created from carrier slots 1 and 2 7. Per-substrate IDS NotConfirmed -> WaitingForHost -> Confirmed 8. PJ + CJ created (E40 + E94, with PPID validator + PJ-membership) 9. CJ Queued -> Selected -> WaitingForStart -> Executing (E94) 10. PJ Queued -> SettingUp -> WaitingForStart -> Processing (E40) 11. Each substrate Acquire -> StartProcessing -> EndProcessing -> Release 12. Module StartGeneral -> StartStep -> CompleteStep (E157) 13. PJ ProcessComplete; CJ AllJobsComplete -> Completed 14. Substrate locations AtDestination + processing Processed 15. E84 unload handshake (CS_0 -> VALID -> U_REQ -> BUSY -> COMPT) 16. LoadPort Unloading -> InService; disassociate 17. EPT Productive -> Standby Total: 278 test cases, 1436 assertions — all green. Any regression in a single FSM that breaks cross-FSM coordination surfaces here. Closes the J-Q tranche set. Repository now exercises the full GEM300 stack from physical I/O (E84) through SECS-II messaging (E5), the equipment model (E30/E120), job management (E40/E94), substrate tracking (E90), carrier/port management (E87), performance tracking (E116), and module process tracking (E157). Co-Authored-By: Claude Opus 4.7 --- CMakeLists.txt | 1 + tests/test_gem300_scenario.cpp | 176 +++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 tests/test_gem300_scenario.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 48968af..9741b35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,6 +115,7 @@ add_executable(secsgem_tests tests/test_sml.cpp tests/test_s9_fallback.cpp tests/test_e84.cpp + tests/test_gem300_scenario.cpp ) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_compile_definitions(secsgem_tests PRIVATE diff --git a/tests/test_gem300_scenario.cpp b/tests/test_gem300_scenario.cpp new file mode 100644 index 0000000..5dfac55 --- /dev/null +++ b/tests/test_gem300_scenario.cpp @@ -0,0 +1,176 @@ +// GEM300 end-to-end conformance scenario: drives every top-level FSM +// through a coordinated lifecycle that mirrors what a real fab acceptance +// test would exercise. No HSMS wire here — the test drives the in-memory +// data model directly so the FSMs and stores are validated as a system. +// +// Scenario: +// 1. Equipment goes Productive (E116 EPT) +// 2. Carrier arrives at LP1 via E84 handshake (load handshake) +// 3. Carrier read: CIDS NotConfirmed -> Confirmed via host ProceedWithCarrier +// 4. Slot map: CSMS NotRead -> Read +// 5. Two substrates created bound to slots 1 and 2 +// 6. Each substrate ID confirmed (E90 IDS) +// 7. Two process jobs created referencing a recipe +// 8. Control job created containing both PJs +// 9. CJ executes: PJs cascade through their lifecycle +// 10. Each substrate goes AtSource -> AtWork -> AtDestination, processing +// goes NeedsProcessing -> InProcess -> Processed +// 11. A module steps through StartGeneral -> StartStep -> CompleteStep +// 12. CJ completes; carrier unloads via E84 (unload handshake) +// 13. Equipment returns to Standby +// +// At every stage we assert the expected state across multiple FSMs so a +// regression in any one of them surfaces here. + +#include + +#include "secsgem/gem/data_model.hpp" +#include "secsgem/gem/e84_state.hpp" +#include "secsgem/gem/ept_state.hpp" +#include "secsgem/gem/substrate_state.hpp" + +using namespace secsgem::gem; + +TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") { + EquipmentDataModel m; + + // Pre-create the recipe the PJs will reference. + m.recipes.add("RECIPE-A", "step1; step2"); + m.load_ports.create(1); + m.modules.create("MOD-A"); + + // ---- 1. Equipment goes Productive -------------------------------------- + CHECK(m.ept.state() == EptState::NonScheduledTime); + m.ept.on_event(EptEvent::EnterStandby); + m.ept.on_event(EptEvent::EnterProductive); + CHECK(m.ept.state() == EptState::Productive); + + // ---- 2. E84 load handshake at LP1 -------------------------------------- + // AMHS asserts CS_0, then VALID; equipment asserts L_REQ; AMHS asserts + // BUSY, then drops BUSY + asserts COMPT. + m.e84.on_signal_change(E84Signal::CS_0, true); + m.e84.on_signal_change(E84Signal::VALID, true); + m.e84.on_signal_change(E84Signal::L_REQ, true); + CHECK(m.e84.state() == E84State::LoadReady); + + m.e84.on_signal_change(E84Signal::BUSY, true); + m.e84.on_signal_change(E84Signal::BUSY, false); + m.e84.on_signal_change(E84Signal::COMPT, true); + CHECK(m.e84.state() == E84State::Complete); + + // Load port transitions on the equipment side. + m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartLoading); + m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteLoading); + + // ---- 3. Carrier read and ID-confirmed --------------------------------- + m.carriers.create("CAR-1", /*port=*/1, /*capacity=*/4); + REQUIRE(m.carriers.has("CAR-1")); + m.load_ports.associate(1, "CAR-1"); + CHECK(m.load_ports.get(1)->fsm->association_state() == + LoadPortAssociationStatus::Associated); + + // Host ProceedWithCarrier -> CIDS::Confirmed. + m.carriers.fire_id_event("CAR-1", CarrierIDEvent::ProceedWithCarrier); + CHECK(m.carriers.get("CAR-1")->fsm->id_status() == CarrierIDStatus::Confirmed); + + // ---- 4. Slot map verified --------------------------------------------- + // Equipment fills the slot map (slots 1 and 2 populated, 3 and 4 empty). + m.carriers.get("CAR-1")->slots[0].state = 1; + m.carriers.get("CAR-1")->slots[1].state = 1; + m.carriers.get("CAR-1")->slots[2].state = 0; + m.carriers.get("CAR-1")->slots[3].state = 0; + m.carriers.fire_slot_map_event("CAR-1", SlotMapEvent::Read); + CHECK(m.carriers.get("CAR-1")->fsm->slot_map_status() == SlotMapStatus::Read); + + // ---- 5. Substrates created from the carrier --------------------------- + REQUIRE(m.substrates.create("W-1", "CAR-1", /*slot=*/1) == + SubstrateStore::CreateResult::Created); + REQUIRE(m.substrates.create("W-2", "CAR-1", /*slot=*/2) == + SubstrateStore::CreateResult::Created); + + // ---- 6. Each substrate's ID is confirmed (E90 IDS) -------------------- + for (auto* id : {"W-1", "W-2"}) { + auto* s = m.substrates.get(id); + REQUIRE(s); + s->fsm->on_id_event(SubstrateIDEvent::Read); + s->fsm->on_id_event(SubstrateIDEvent::Confirm); + CHECK(s->fsm->id_state() == SubstrateIDStatus::Confirmed); + } + + // ---- 7-8. Process jobs + Control job --------------------------------- + CHECK(m.process_jobs.create( + "PJ-1", "RECIPE-A", {"W-1", "W-2"}, + [&](const std::string& ppid) { return m.recipes.get(ppid).has_value(); }) == + ProcessJobStore::CreateResult::Created); + CHECK(m.control_jobs.create("CJ-1", {"PJ-1"}, + [&](const std::string& id) { + return m.process_jobs.has(id); + }) == ControlJobStore::CreateResult::Created); + + // ---- 9-11. CJ executes; PJs + substrates + module cascade ----------- + m.control_jobs.fire_internal("CJ-1", ControlJobEvent::Select); + m.control_jobs.fire_internal("CJ-1", ControlJobEvent::SetupComplete); + m.control_jobs.fire_internal("CJ-1", ControlJobEvent::Start); + CHECK(m.control_jobs.get("CJ-1")->fsm->state() == ControlJobState::Executing); + + m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::Select); + m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::SetupComplete); + m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::Start); + CHECK(m.process_jobs.get("PJ-1")->fsm->state() == ProcessJobState::Processing); + + // Per-substrate processing cascade. + for (auto* id : {"W-1", "W-2"}) { + m.substrates.fire_location_event(id, SubstrateEvent::Acquire, "MOD-A"); + } + + // Module steps through. + m.modules.bind("MOD-A", "W-1", "step1"); + m.modules.fire("MOD-A", ModuleEvent::StartGeneral); + m.modules.fire("MOD-A", ModuleEvent::StartStep); + + for (auto* id : {"W-1", "W-2"}) { + m.substrates.fire_processing_event(id, SubstrateProcessingEvent::StartProcessing); + m.substrates.fire_processing_event(id, SubstrateProcessingEvent::EndProcessing); + m.substrates.fire_location_event(id, SubstrateEvent::Release, "DEST"); + } + + m.modules.fire("MOD-A", ModuleEvent::CompleteStep); + CHECK(m.modules.get("MOD-A")->fsm->state() == ModuleState::StepCompleted); + + m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::ProcessComplete); + CHECK(m.process_jobs.get("PJ-1")->fsm->state() == ProcessJobState::ProcessComplete); + + m.control_jobs.fire_internal("CJ-1", ControlJobEvent::AllJobsComplete); + CHECK(m.control_jobs.get("CJ-1")->fsm->state() == ControlJobState::Completed); + + // Each substrate is at destination and processed. + for (auto* id : {"W-1", "W-2"}) { + CHECK(m.substrates.get(id)->fsm->location_state() == SubstrateState::AtDestination); + CHECK(m.substrates.get(id)->fsm->processing_state() == + SubstrateProcessingState::Processed); + // History should have recorded both axes' transitions. + CHECK(m.substrates.history(id)->size() >= 4); + } + + // ---- 12. Carrier unloads via E84 ------------------------------------- + m.e84.reset(); + m.e84.on_signal_change(E84Signal::CS_0, true); + m.e84.on_signal_change(E84Signal::VALID, true); + m.e84.on_signal_change(E84Signal::U_REQ, true); + CHECK(m.e84.state() == E84State::UnloadReady); + m.e84.on_signal_change(E84Signal::BUSY, true); + m.e84.on_signal_change(E84Signal::BUSY, false); + m.e84.on_signal_change(E84Signal::COMPT, true); + CHECK(m.e84.state() == E84State::Complete); + + m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartUnloading); + m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteUnloading); + m.load_ports.disassociate(1); + + // ---- 13. Equipment returns to Standby -------------------------------- + m.ept.on_event(EptEvent::EnterStandby); + CHECK(m.ept.state() == EptState::Standby); + + // Time accounting should show productive time accumulated. + CHECK(m.ept.accumulated(EptState::Productive).count() >= 0); +}