diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.BoundaryEvents.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.BoundaryEvents.cs new file mode 100644 index 000000000..936a823d0 --- /dev/null +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.BoundaryEvents.cs @@ -0,0 +1,123 @@ +using Bpmn.Model; +using Elsa.Bpmn.Activities; +using Elsa.Bpmn.IntegrationTests.Scenarios.HostPort.Activities; + +namespace Elsa.Bpmn.IntegrationTests.Scenarios.HostPort; + +internal static partial class BpmnTestProcesses +{ + /// An interrupting timer boundary event on a long-running task. + public static BpmnProcess InterruptingTimerBoundary(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("interrupting-timer-boundary") + .StartEvent("start") + .Task("task", bindingRef: BindingRef("task")) + .EndEvent("end") + .BoundaryEvent("timeout", attachedTo: "task", eventDefinition: Timer(), interrupting: true, bindingRef: BindingRef("timeout")) + .Task("onTimeout", bindingRef: BindingRef("onTimeout")) + .EndEvent("timedOut") + .ConnectSequence("start", "task", "end") + .ConnectSequence("timeout", "onTimeout", "timedOut") + .Build(); + + return Scope("scope", definition, Blocking("task", log), Blocking("timeout", log), Immediate("onTimeout", log)); + } + + /// A task that fails, with an error boundary event that catches it. + public static BpmnProcess ErrorBoundaryCaught(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("error-boundary-caught") + .StartEvent("start") + .Task("risky", bindingRef: BindingRef("risky")) + .EndEvent("end") + .BoundaryEvent("oops", attachedTo: "risky", eventDefinition: new BpmnEventDefinition(BpmnEventDefinitionTypes.Error)) + .Task("recover", bindingRef: BindingRef("recover")) + .EndEvent("recovered") + .ConnectSequence("start", "risky", "end") + .ConnectSequence("oops", "recover", "recovered") + .Build(); + + return Scope("scope", definition, Faulting("risky", log), Immediate("recover", log)); + } + + /// + /// A task that fails inside an embedded subprocess with nothing there to catch it, and an error boundary event on + /// the subprocess in the enclosing scope that does. + /// + public static BpmnProcess ErrorPropagatedOutOfSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("failing-subprocess-body") + .StartEvent("subStart") + .Task("subRisky", bindingRef: BindingRef("subRisky")) + .EndEvent("subEnd") + .ConnectSequence("subStart", "subRisky", "subEnd") + .Build(); + + var definition = new BpmnProcessBuilder("error-propagated-out-of-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .BoundaryEvent("subOops", attachedTo: "sub", eventDefinition: new BpmnEventDefinition(BpmnEventDefinitionTypes.Error)) + .Task("subRecover", bindingRef: BindingRef("subRecover")) + .EndEvent("subRecovered") + .ConnectSequence("start", "sub", "after", "end") + .ConnectSequence("subOops", "subRecover", "subRecovered") + .Build(); + + var nested = Scope("sub", body, Faulting("subRisky", log)); + + return Scope("scope", definition, nested, Immediate("after", log), Immediate("subRecover", log)); + } + + /// A task that fails with nothing to catch it. + public static BpmnProcess UncaughtError(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("uncaught-error") + .StartEvent("start") + .Task("risky", bindingRef: BindingRef("risky")) + .EndEvent("end") + .ConnectSequence("start", "risky", "end") + .Build(); + + return Scope("scope", definition, Faulting("risky", log)); + } + + /// + /// An escalation thrown out of an embedded subprocess, caught by a non-interrupting escalation boundary event on + /// the subprocess. The nested scope keeps running, which is what "non-interrupting" means. + /// + public static BpmnProcess EscalationOutOfSubprocess(BpmnTestLog log) + { + // subFirst runs before anything the parent could be confused with, deliberately: it puts the nested scope's + // handle counter ahead of the parent's. A host that recognised its work by a shared, rewritable key rather than + // by the child activity execution would otherwise be rescued by two independent counters happening to agree. + var body = new BpmnProcessBuilder("subprocess-body") + .StartEvent("subStart") + .Task("subFirst", bindingRef: BindingRef("subFirst")) + .Task("subWork", bindingRef: BindingRef("subWork")) + .IntermediateThrowEvent("subEscalate", Escalation("REVIEW")) + .Task("subMore", bindingRef: BindingRef("subMore")) + .EndEvent("subEnd") + .ConnectSequence("subStart", "subFirst", "subWork", "subEscalate", "subMore", "subEnd") + .Build(); + + var definition = new BpmnProcessBuilder("escalation-out-of-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .BoundaryEvent("escalated", attachedTo: "sub", eventDefinition: Escalation("REVIEW"), interrupting: false) + .Task("notify", bindingRef: BindingRef("notify")) + .EndEvent("notified") + .ConnectSequence("start", "sub", "after", "end") + .ConnectSequence("escalated", "notify", "notified") + .Build(); + + // subMore blocks so the subprocess is still running when the escalation path executes, which is what makes + // "the escalating work is still live" observable rather than merely asserted. + var nested = Scope("sub", body, Immediate("subFirst", log), Blocking("subWork", log), Blocking("subMore", log)); + + return Scope("scope", definition, nested, Immediate("after", log), Immediate("notify", log)); + } +} diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.Compensation.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.Compensation.cs new file mode 100644 index 000000000..fa0983d98 --- /dev/null +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.Compensation.cs @@ -0,0 +1,256 @@ +using Bpmn.Model; +using Elsa.Bpmn.Activities; +using Elsa.Bpmn.IntegrationTests.Scenarios.HostPort.Activities; + +namespace Elsa.Bpmn.IntegrationTests.Scenarios.HostPort; + +internal static partial class BpmnTestProcesses +{ + /// + /// Three bookings, each carrying a compensation boundary event, and a compensate end event that replays the lot. + /// + /// + /// Three rather than two: with two, a handler order that merely happened to be reversed is indistinguishable from + /// one that swapped a pair, and a replay that walked the log forwards would still run every handler. The three + /// handlers bind work like anything else, and nothing in the graph flows into them — the only thing that can run + /// them is the replay. + /// + public static BpmnProcess CompensatedBookings(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("compensated-bookings") + .StartEvent("start") + .Task("bookFlight", bindingRef: BindingRef("bookFlight")) + .Task("bookHotel", bindingRef: BindingRef("bookHotel")) + .Task("bookCar", bindingRef: BindingRef("bookCar")) + .EndEvent("undoEverything", null, Compensation()) + .Element(CompensationBoundary("flightCompensated", attachedTo: "bookFlight", handler: "undoFlight")) + .Element(CompensationBoundary("hotelCompensated", attachedTo: "bookHotel", handler: "undoHotel")) + .Element(CompensationBoundary("carCompensated", attachedTo: "bookCar", handler: "undoCar")) + .Element(CompensationHandler("undoFlight")) + .Element(CompensationHandler("undoHotel")) + .Element(CompensationHandler("undoCar")) + .ConnectSequence("start", "bookFlight", "bookHotel", "bookCar", "undoEverything") + .Build(); + + return Scope( + "scope", + definition, + Immediate("bookFlight", log), + Immediate("bookHotel", log), + Immediate("bookCar", log), + Immediate("undoFlight", log), + Immediate("undoHotel", log), + Immediate("undoCar", log)); + } + + /// + /// The same three bookings, compensated by an intermediate throw event naming one of them in its + /// activityRef, and a task after the throw that its outbound flow reaches once the replay is done. + /// + public static BpmnProcess TargetedCompensation(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("targeted-compensation") + .StartEvent("start") + .Task("bookFlight", bindingRef: BindingRef("bookFlight")) + .Task("bookHotel", bindingRef: BindingRef("bookHotel")) + .Task("bookCar", bindingRef: BindingRef("bookCar")) + .IntermediateThrowEvent("undoHotelOnly", Compensation(activityRef: "bookHotel")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .Element(CompensationBoundary("flightCompensated", attachedTo: "bookFlight", handler: "undoFlight")) + .Element(CompensationBoundary("hotelCompensated", attachedTo: "bookHotel", handler: "undoHotel")) + .Element(CompensationBoundary("carCompensated", attachedTo: "bookCar", handler: "undoCar")) + .Element(CompensationHandler("undoFlight")) + .Element(CompensationHandler("undoHotel")) + .Element(CompensationHandler("undoCar")) + .ConnectSequence("start", "bookFlight", "bookHotel", "bookCar", "undoHotelOnly", "after", "end") + .Build(); + + return Scope( + "scope", + definition, + Immediate("bookFlight", log), + Immediate("bookHotel", log), + Immediate("bookCar", log), + Immediate("undoFlight", log), + Immediate("undoHotel", log), + Immediate("undoCar", log), + Immediate("after", log)); + } + + /// + /// A compensation log inside an embedded subprocess, and a second one in the enclosing scope that compensates the + /// subprocess itself. + /// + /// + /// Two logs, one per scope, and neither can see the other: the body replays its own two handlers before it + /// completes, and the enclosing scope registers exactly one entry — the subprocess's own successful completion, + /// which its attached compensation boundary makes compensable — and replays that. + /// + public static BpmnProcess CompensationInsideSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("compensating-subprocess-body") + .StartEvent("subStart") + .Task("subCharge", bindingRef: BindingRef("subCharge")) + .Task("subShip", bindingRef: BindingRef("subShip")) + .EndEvent("subUndo", null, Compensation()) + .Element(CompensationBoundary("subChargeCompensated", attachedTo: "subCharge", handler: "subRefund")) + .Element(CompensationBoundary("subShipCompensated", attachedTo: "subShip", handler: "subRecall")) + .Element(CompensationHandler("subRefund")) + .Element(CompensationHandler("subRecall")) + .ConnectSequence("subStart", "subCharge", "subShip", "subUndo") + .Build(); + + var definition = new BpmnProcessBuilder("compensation-inside-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .EndEvent("undoOuter", null, Compensation()) + .Element(CompensationBoundary("subCompensated", attachedTo: "sub", handler: "undoSub")) + .Element(CompensationHandler("undoSub")) + .ConnectSequence("start", "sub", "undoOuter") + .Build(); + + var nested = Scope( + "sub", + body, + Immediate("subCharge", log), + Immediate("subShip", log), + Immediate("subRefund", log), + Immediate("subRecall", log)); + + return Scope("scope", definition, nested, Immediate("undoSub", log)); + } + + /// + /// A transaction subprocess that cancels itself from the inside, and a cancel boundary event on the transaction + /// that routes the cancellation. + /// + /// + /// The nested scope completes with the Cancelled outcome rather than Done, and the enclosing scope + /// only reaches the boundary path if that outcome survives the trip through the parent's completion callback. + /// Nothing else in the process distinguishes the two: with the outcome dropped the parent simply carries on down + /// the ordinary sequence flow, which is a completion that looks entirely successful. + /// + public static BpmnProcess CancelledTransactionSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("transaction-body") + .Transaction() + .StartEvent("subStart") + .Task("subWork", bindingRef: BindingRef("subWork")) + .EndEvent("subCancelled", null, Cancel()) + .ConnectSequence("subStart", "subWork", "subCancelled") + .Build(); + + var definition = new BpmnProcessBuilder("cancelled-transaction-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub"), isTransaction: true) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .BoundaryEvent("cancelled", attachedTo: "sub", eventDefinition: Cancel()) + .Task("unwind", bindingRef: BindingRef("unwind")) + .EndEvent("unwound") + .ConnectSequence("start", "sub", "after", "end") + .ConnectSequence("cancelled", "unwind", "unwound") + .Build(); + + var nested = Scope("sub", body, Immediate("subWork", log)); + + return Scope("scope", definition, nested, Immediate("after", log), Immediate("unwind", log)); + } + + /// + /// A transaction subprocess that cancels itself from the inside, with nothing on the enclosing scope to route the + /// cancellation. + /// + /// + /// The same shape as minus the cancel boundary event. Graph + /// validation cannot see into the nested definition to know a cancel end event is in there, so an unroutable + /// cancellation is an execution-time rule: the enclosing scope faults rather than treating the transaction as an + /// ordinary completion and carrying on down the sequence flow. + /// + public static BpmnProcess CancelledTransactionWithoutCancelBoundary(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("unroutable-cancel-body") + .Transaction() + .StartEvent("subStart") + .Task("subWork", bindingRef: BindingRef("subWork")) + .EndEvent("subCancelled", null, Cancel()) + .ConnectSequence("subStart", "subWork", "subCancelled") + .Build(); + + var definition = new BpmnProcessBuilder("unroutable-cancel") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub"), isTransaction: true) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "sub", "after", "end") + .Build(); + + var nested = Scope("sub", body, Immediate("subWork", log)); + + return Scope("scope", definition, nested, Immediate("after", log)); + } + + /// + /// A transaction subprocess that starts a compensation replay on one branch and cancels itself on the other while + /// that replay is still running, so the replay's claimed-but-unrun log entries are torn down mid-run. + /// + /// + /// + /// The shape is what makes the release observable. chargeCard and reserveSeat both complete and + /// register, in that order, so the replay the intermediate throw opens claims both and runs them in reverse: + /// releaseSeat first, which blocks, leaving refundCard claimed and never started. Cancelling the + /// transaction from the other branch stops the replay's coordinating token, which is the only thing in scope that + /// tears a run down mid-flight. + /// + /// + /// fraudCheck blocks so a test decides when the cancellation happens, rather than racing the replay. + /// + /// + public static BpmnProcess CompensationRunCancelledMidReplay(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("mid-replay-cancel-body") + .Transaction() + .StartEvent("subStart") + .ParallelGateway("subSplit") + .Task("chargeCard", bindingRef: BindingRef("chargeCard")) + .Task("reserveSeat", bindingRef: BindingRef("reserveSeat")) + .IntermediateThrowEvent("rollBack", Compensation()) + .EndEvent("rolledBack") + .Task("fraudCheck", bindingRef: BindingRef("fraudCheck")) + .EndEvent("subCancelled", null, Cancel()) + .Element(CompensationBoundary("cardCompensated", attachedTo: "chargeCard", handler: "refundCard")) + .Element(CompensationBoundary("seatCompensated", attachedTo: "reserveSeat", handler: "releaseSeat")) + .Element(CompensationHandler("refundCard")) + .Element(CompensationHandler("releaseSeat")) + .ConnectSequence("subStart", "subSplit") + .Connect("subSplit", "chargeCard") + .ConnectSequence("chargeCard", "reserveSeat", "rollBack", "rolledBack") + .Connect("subSplit", "fraudCheck") + .ConnectSequence("fraudCheck", "subCancelled") + .Build(); + + var definition = new BpmnProcessBuilder("mid-replay-cancel") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub"), isTransaction: true) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .BoundaryEvent("cancelled", attachedTo: "sub", eventDefinition: Cancel()) + .Task("unwind", bindingRef: BindingRef("unwind")) + .EndEvent("unwound") + .ConnectSequence("start", "sub", "after", "end") + .ConnectSequence("cancelled", "unwind", "unwound") + .Build(); + + var nested = Scope( + "sub", + body, + Immediate("chargeCard", log), + Immediate("reserveSeat", log), + Blocking("fraudCheck", log), + Immediate("refundCard", log), + Blocking("releaseSeat", log)); + + return Scope("scope", definition, nested, Immediate("after", log), Immediate("unwind", log)); + } +} diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.EventSubprocesses.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.EventSubprocesses.cs new file mode 100644 index 000000000..fb3fd95fe --- /dev/null +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.EventSubprocesses.cs @@ -0,0 +1,297 @@ +using Bpmn.Model; +using Elsa.Bpmn.Activities; +using Elsa.Bpmn.IntegrationTests.Scenarios.HostPort.Activities; +using Elsa.Workflows; + +namespace Elsa.Bpmn.IntegrationTests.Scenarios.HostPort; + +internal static partial class BpmnTestProcesses +{ + /// + /// A task that fails, with a dormant error-triggered event subprocess in the same scope to catch it. + /// + /// + /// An error event subprocess arms nothing: it rides the same FaultSignal seam an error boundary event does, + /// and the only thing that distinguishes it here is where the recovery work runs — inside a nested scope of its + /// own, seeded at the body's error start event, rather than on an outbound flow of the enclosing graph. + /// + public static BpmnProcess ErrorEventSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("error-event-subprocess-body") + .Element(EventSubprocessStart("errStart", Error())) + .Task("handleError", bindingRef: BindingRef("handleError")) + .EndEvent("errEnd") + .ConnectSequence("errStart", "handleError", "errEnd") + .Build(); + + var definition = new BpmnProcessBuilder("error-event-subprocess") + .StartEvent("start") + .Task("risky", bindingRef: BindingRef("risky")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .Element(EventSubprocess("evtSub")) + .ConnectSequence("start", "risky", "after", "end") + .Build(); + + return Scope("scope", definition, Faulting("risky", log), Immediate("after", log), Scope("evtSub", body, Immediate("handleError", log))); + } + + /// + /// An escalation thrown out of an embedded subprocess, caught by a non-interrupting escalation-triggered event + /// subprocess on the enclosing scope rather than by a boundary event on the subprocess. + /// + /// + /// Non-interrupting, so the escalating subprocess keeps running and nothing in the scope is torn down. That is + /// also what makes "the scope-level catcher fired" distinguishable from "the subprocess was stopped": with an + /// interrupting catcher the two are the same observation. + /// + public static BpmnProcess EscalationEventSubprocessOutOfSubprocess(BpmnTestLog log) + { + var subBody = new BpmnProcessBuilder("escalating-subprocess-body") + .StartEvent("subStart") + .Task("subWork", bindingRef: BindingRef("subWork")) + .IntermediateThrowEvent("subEscalate", Escalation("REVIEW")) + .Task("subMore", bindingRef: BindingRef("subMore")) + .EndEvent("subEnd") + .ConnectSequence("subStart", "subWork", "subEscalate", "subMore", "subEnd") + .Build(); + + var handlerBody = new BpmnProcessBuilder("escalation-event-subprocess-body") + .Element(EventSubprocessStart("escStart", Escalation("REVIEW"), interrupting: false)) + .Task("handleEscalation", bindingRef: BindingRef("handleEscalation")) + .EndEvent("escEnd") + .ConnectSequence("escStart", "handleEscalation", "escEnd") + .Build(); + + var definition = new BpmnProcessBuilder("escalation-event-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .Element(EventSubprocess("evtSub")) + .ConnectSequence("start", "sub", "after", "end") + .Build(); + + var nested = Scope("sub", subBody, Blocking("subWork", log), Blocking("subMore", log)); + + return Scope("scope", definition, nested, Immediate("after", log), Scope("evtSub", handlerBody, Immediate("handleEscalation", log))); + } + + /// + /// A non-interrupting message-triggered event subprocess: a listener armed at scope start, and a body that runs + /// each time the listener fires while the scope's own long-running work is still going. + /// + /// + /// + /// The listener is the second binding channel — listenerBindingRef — and is bound in the same + /// WorkBindings map as everything else. It stands in for a real message wait: blocking work a test + /// finishes, which is exactly what "the trigger fired" means to the host. + /// + /// + /// work blocks so the scope stays open across the fires and so that when it finally completes, the armed + /// listener is a running activity rather than a scheduled-but-not-yet-invoked one — the second of which + /// this host cannot withdraw at all. + /// + /// + public static BpmnProcess MessageEventSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("message-event-subprocess-body") + .Element(EventSubprocessStart("msgStart", Message("nudge"), interrupting: false)) + .Task("handleNudge", bindingRef: BindingRef("handleNudge")) + .EndEvent("msgEnd") + .ConnectSequence("msgStart", "handleNudge", "msgEnd") + .Build(); + + var definition = new BpmnProcessBuilder("message-event-subprocess") + .StartEvent("start") + .Task("work", bindingRef: BindingRef("work")) + .EndEvent("end") + .Element(EventSubprocess("evtSub", listenerBindingRef: BindingRef("nudgeListener"))) + .ConnectSequence("start", "work", "end") + .Build(); + + return Scope( + "scope", + definition, + Blocking("work", log), + Blocking("nudgeListener", log), + Scope("evtSub", body, Immediate("handleNudge", log))); + } + + /// + /// The same message-triggered event subprocess, but inside an embedded subprocess that completes while the + /// enclosing scope carries on — so a listener that outlived the scope that armed it is distinguishable from one + /// that merely outlived the workflow. + /// + /// + /// At the root, "the armed work does not survive the scope" and "does not survive the workflow" are the same + /// observation, and Elsa tears a finished workflow's children down regardless. Here the workflow keeps running + /// after the scope that armed the listener has completed, which is the only shape in which a listener left behind + /// is a listener something could still resume into. + /// + public static BpmnProcess NestedMessageEventSubprocess(BpmnTestLog log) + { + var handlerBody = new BpmnProcessBuilder("nested-message-event-subprocess-body") + .Element(EventSubprocessStart("msgStart", Message("nudge"), interrupting: false)) + .Task("handleNudge", bindingRef: BindingRef("handleNudge")) + .EndEvent("msgEnd") + .ConnectSequence("msgStart", "handleNudge", "msgEnd") + .Build(); + + var subBody = new BpmnProcessBuilder("listening-subprocess-body") + .StartEvent("subStart") + .Task("subWork", bindingRef: BindingRef("subWork")) + .EndEvent("subEnd") + .Element(EventSubprocess("evtSub", listenerBindingRef: BindingRef("nudgeListener"))) + .ConnectSequence("subStart", "subWork", "subEnd") + .Build(); + + var definition = new BpmnProcessBuilder("nested-message-event-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "sub", "after", "end") + .Build(); + + var nested = Scope( + "sub", + subBody, + Blocking("subWork", log), + Blocking("nudgeListener", log), + Scope("evtSub", handlerBody, Immediate("handleNudge", log))); + + return Scope("scope", definition, nested, Immediate("after", log)); + } + + /// + /// An error-triggered event subprocess whose body runs an ordinary embedded subprocess of its own, so the + /// start-element hint has both a place to arrive and a place it must not reach. + /// + /// + /// + /// The body's only start event is event-defined, which is what makes the hint's arrival observable rather than + /// merely asserted: seeded from the hint the body runs, and seeded as an ordinary direct invocation it faults + /// deterministically with bpmn.start.none-available, because there is no none start event to begin at. + /// + /// + /// The nested inner subprocess is the other direction. Its own invocation carries an ordinary scheduling + /// cause, so the hint must not be inherited: were it, the inner process would be seeded at an element it does not + /// declare and fault with bpmn.start.unresolved-hint instead of starting at its own none start event. + /// + /// + public static BpmnProcess EventSubprocessBodyWithNestedSubprocess(BpmnTestLog log) + { + var innerBody = new BpmnProcessBuilder("event-subprocess-inner-body") + .StartEvent("innerStart") + .Task("innerOnly", bindingRef: BindingRef("innerOnly")) + .EndEvent("innerEnd") + .ConnectSequence("innerStart", "innerOnly", "innerEnd") + .Build(); + + var body = new BpmnProcessBuilder("hinted-event-subprocess-body") + .Element(EventSubprocessStart("errStart", Error())) + .Task("handleError", bindingRef: BindingRef("handleError")) + .SubProcess("inner", bindingRef: BindingRef("inner")) + .EndEvent("errEnd") + .ConnectSequence("errStart", "handleError", "inner", "errEnd") + .Build(); + + var definition = new BpmnProcessBuilder("event-subprocess-start-hint") + .StartEvent("start") + .Task("risky", bindingRef: BindingRef("risky")) + .EndEvent("end") + .Element(EventSubprocess("evtSub")) + .ConnectSequence("start", "risky", "end") + .Build(); + + var handler = Scope("evtSub", body, Immediate("handleError", log), Scope("inner", innerBody, Immediate("innerOnly", log))); + + return Scope("scope", definition, Faulting("risky", log), handler); + } + + /// An event subprocess whose body declares two start events, which the library refuses. + public static BpmnProcess EventSubprocessBodyWithTwoStartEvents(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("two-start-event-subprocess-body") + .Element(EventSubprocessStart("errStart", Error())) + .StartEvent("alsoStart") + .Task("handleError", bindingRef: BindingRef("handleError")) + .EndEvent("errEnd") + .ConnectSequence("errStart", "handleError", "errEnd") + .Connect("alsoStart", "handleError") + .Build(); + + return RefusedEventSubprocessScope("two-start-events", log, ("evtSub", body, "handleError")); + } + + /// Two error-triggered event subprocesses in one scope, which the library refuses. + public static BpmnProcess TwoErrorEventSubprocesses(BpmnTestLog log) + { + BpmnProcessDefinition Body(string prefix) => new BpmnProcessBuilder($"{prefix}-error-event-subprocess-body") + .Element(EventSubprocessStart($"{prefix}Start", Error())) + .Task($"{prefix}Handle", bindingRef: BindingRef($"{prefix}Handle")) + .EndEvent($"{prefix}End") + .ConnectSequence($"{prefix}Start", $"{prefix}Handle", $"{prefix}End") + .Build(); + + return RefusedEventSubprocessScope( + "two-error-event-subprocesses", + log, + ("evtSubA", Body("first"), "firstHandle"), + ("evtSubB", Body("second"), "secondHandle")); + } + + /// Two code-less catch-all escalation-triggered event subprocesses in one scope, which the library refuses. + public static BpmnProcess TwoCatchAllEscalationEventSubprocesses(BpmnTestLog log) + { + BpmnProcessDefinition Body(string prefix) => new BpmnProcessBuilder($"{prefix}-escalation-event-subprocess-body") + .Element(EventSubprocessStart($"{prefix}Start", Escalation(), interrupting: false)) + .Task($"{prefix}Handle", bindingRef: BindingRef($"{prefix}Handle")) + .EndEvent($"{prefix}End") + .ConnectSequence($"{prefix}Start", $"{prefix}Handle", $"{prefix}End") + .Build(); + + return RefusedEventSubprocessScope( + "two-catch-all-escalation-event-subprocesses", + log, + ("evtSubA", Body("first"), "firstHandle"), + ("evtSubB", Body("second"), "secondHandle")); + } + + /// A non-interrupting error-triggered event subprocess, which is not legal BPMN and which the library refuses. + public static BpmnProcess NonInterruptingErrorEventSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("non-interrupting-error-event-subprocess-body") + .Element(EventSubprocessStart("errStart", Error(), interrupting: false)) + .Task("handleError", bindingRef: BindingRef("handleError")) + .EndEvent("errEnd") + .ConnectSequence("errStart", "handleError", "errEnd") + .Build(); + + return RefusedEventSubprocessScope("non-interrupting-error-event-subprocess", log, ("evtSub", body, "handleError")); + } + + /// + /// The start/only/end graph the refusal processes share, carrying the event subprocesses whose declaration + /// the library refuses. Nothing in it ever runs: the refusal is raised when the scope builds its graph, which is + /// before any work is started. + /// + private static BpmnProcess RefusedEventSubprocessScope(string processId, BpmnTestLog log, params (string ElementId, BpmnProcessDefinition Body, string HandlerId)[] eventSubprocesses) + { + var builder = new BpmnProcessBuilder(processId) + .StartEvent("start") + .Task("only", bindingRef: BindingRef("only")) + .EndEvent("end") + .ConnectSequence("start", "only", "end"); + + foreach (var eventSubprocess in eventSubprocesses) + builder = builder.Element(EventSubprocess(eventSubprocess.ElementId)); + + var work = new List { Immediate("only", log) }; + + work.AddRange(eventSubprocesses.Select(eventSubprocess => Scope(eventSubprocess.ElementId, eventSubprocess.Body, Immediate(eventSubprocess.HandlerId, log)))); + + return Scope("scope", builder.Build(), work.ToArray()); + } +} diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.Flow.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.Flow.cs new file mode 100644 index 000000000..97da7abcc --- /dev/null +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.Flow.cs @@ -0,0 +1,116 @@ +using Bpmn.Model; +using Elsa.Bpmn.Activities; +using Elsa.Bpmn.IntegrationTests.Scenarios.HostPort.Activities; +using Elsa.Workflows; + +namespace Elsa.Bpmn.IntegrationTests.Scenarios.HostPort; + +internal static partial class BpmnTestProcesses +{ + /// A linear process: one task between a start and an end event. + public static BpmnProcess LinearTask(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("linear-task") + .StartEvent("start") + .Task("only", bindingRef: BindingRef("only")) + .EndEvent("end") + .ConnectSequence("start", "only", "end") + .Build(); + + return Scope("scope", definition, Immediate("only", log)); + } + + /// An embedded subprocess with one task in it, and one task after it in the enclosing scope. + public static BpmnProcess NestedSubprocess(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("nested-subprocess-body") + .StartEvent("subStart") + .Task("subOnly", bindingRef: BindingRef("subOnly")) + .EndEvent("subEnd") + .ConnectSequence("subStart", "subOnly", "subEnd") + .Build(); + + var definition = new BpmnProcessBuilder("nested-subprocess") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "sub", "after", "end") + .Build(); + + return Scope("scope", definition, Scope("sub", body, Immediate("subOnly", log)), Immediate("after", log)); + } + + /// A parallel gateway split and join. + public static BpmnProcess ParallelSplitAndJoin(BpmnTestLog log) => + ParallelSplitAndJoinTopology("parallel-split-and-join", Immediate("left", log), Immediate("right", log), log); + + /// + /// A parallel split into two branches that both block, and a join. Used to prove a scope suspends with two live + /// units of work outstanding and, once resumed, matches each completion back to its own binding through the + /// rehydrated ledger. + /// + public static BpmnProcess ParallelSplitAndJoinBlocking(BpmnTestLog log) => + ParallelSplitAndJoinTopology("parallel-split-and-join-blocking", Blocking("left", log), Blocking("right", log), log); + + /// + /// The start/split/left/right/join/after/end graph shared by and + /// , parameterised by the work the two branches run. + /// + private static BpmnProcess ParallelSplitAndJoinTopology(string processId, IActivity leftWork, IActivity rightWork, BpmnTestLog log) + { + var definition = new BpmnProcessBuilder(processId) + .StartEvent("start") + .ParallelGateway("split") + .Task("left", bindingRef: BindingRef("left")) + .Task("right", bindingRef: BindingRef("right")) + .ParallelGateway("join") + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "split") + .Connect("split", "left") + .Connect("split", "right") + .Connect("left", "join") + .Connect("right", "join") + .ConnectSequence("join", "after", "end") + .Build(); + + return Scope("scope", definition, leftWork, rightWork, Immediate("after", log)); + } + + /// + /// A parallel split and join, blocking on both branches, nested inside an embedded subprocess. Used to prove + /// a nested scope's own ledger -- not just a root scope's -- matches each completion back to its + /// binding after a round trip through Elsa's own serializer. + /// + public static BpmnProcess NestedParallelSplitAndJoinBlocking(BpmnTestLog log) + { + var body = new BpmnProcessBuilder("nested-parallel-split-and-join-body") + .StartEvent("subStart") + .ParallelGateway("subSplit") + .Task("subLeft", bindingRef: BindingRef("subLeft")) + .Task("subRight", bindingRef: BindingRef("subRight")) + .ParallelGateway("subJoin") + .Task("subAfter", bindingRef: BindingRef("subAfter")) + .EndEvent("subEnd") + .ConnectSequence("subStart", "subSplit") + .Connect("subSplit", "subLeft") + .Connect("subSplit", "subRight") + .Connect("subLeft", "subJoin") + .Connect("subRight", "subJoin") + .ConnectSequence("subJoin", "subAfter", "subEnd") + .Build(); + + var definition = new BpmnProcessBuilder("nested-parallel-split-and-join-blocking") + .StartEvent("start") + .SubProcess("sub", bindingRef: BindingRef("sub")) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "sub", "after", "end") + .Build(); + + var nested = Scope("sub", body, Blocking("subLeft", log), Blocking("subRight", log), Immediate("subAfter", log)); + + return Scope("scope", definition, nested, Immediate("after", log)); + } +} diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.MultiInstance.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.MultiInstance.cs new file mode 100644 index 000000000..be035fb68 --- /dev/null +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.MultiInstance.cs @@ -0,0 +1,69 @@ +using Bpmn.Model; +using Elsa.Bpmn.Activities; +using Elsa.Bpmn.IntegrationTests.Scenarios.HostPort.Activities; +using Elsa.Workflows.Memory; + +namespace Elsa.Bpmn.IntegrationTests.Scenarios.HostPort; + +internal static partial class BpmnTestProcesses +{ + /// The name of the variable loops over. + public const string CollectionVariableName = "items"; + + /// + /// A parallel multi-instance task: two concurrent instances of one binding, told apart only by their iteration id. + /// + public static BpmnProcess ParallelMultiInstanceTask(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("parallel-multi-instance-task") + .StartEvent("start") + .Task(BpmnElementTypes.Task, "each", bindingRef: BindingRef("each"), loopCharacteristics: new BpmnLoopCharacteristics(isSequential: false, cardinality: 2)) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "each", "after", "end") + .Build(); + + return Scope("scope", definition, Blocking("each", log), Immediate("after", log)); + } + + /// + /// A sequential multi-instance task: one instance at a time, each blocking until a test finishes it. Used to + /// drive many evaluations of one scope so a persisted blob that is not pruned is observable as unbounded growth. + /// + public static BpmnProcess SequentialMultiInstanceTask(BpmnTestLog log, int cardinality) + { + var definition = new BpmnProcessBuilder("sequential-multi-instance-task") + .StartEvent("start") + .Task(BpmnElementTypes.Task, "each", bindingRef: BindingRef("each"), loopCharacteristics: new BpmnLoopCharacteristics(isSequential: true, cardinality: cardinality)) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "each", "after", "end") + .Build(); + + return Scope("scope", definition, Blocking("each", log), Immediate("after", log)); + } + + /// + /// A collection-mode multi-instance task: one instance per item of a container-scoped variable, which the + /// interpreter reads back through IBpmnVariableReader while it evaluates. + /// + /// + /// Three items rather than two, so the instance count cannot be confused with a declared cardinality. The + /// collection variable is declared on both sides — on the definition, because BpmnGraph.Build refuses a + /// loop naming a variable the process does not declare, and on the activity, because that is where the value + /// actually lives. + /// + public static BpmnProcess CollectionMultiInstanceTask(BpmnTestLog log) + { + var definition = new BpmnProcessBuilder("collection-multi-instance-task") + .Variable(CollectionVariableName) + .StartEvent("start") + .Task(BpmnElementTypes.Task, "each", bindingRef: BindingRef("each"), loopCharacteristics: new BpmnLoopCharacteristics(isSequential: false, collectionVariable: CollectionVariableName)) + .Task("after", bindingRef: BindingRef("after")) + .EndEvent("end") + .ConnectSequence("start", "each", "after", "end") + .Build(); + + return Scope("scope", definition, [new Variable(CollectionVariableName, ["alpha", "beta", "gamma"])], Immediate("each", log), Immediate("after", log)); + } +} diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.cs index 024c32629..2d0b0fdba 100644 --- a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.cs +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnTestProcesses.cs @@ -11,831 +11,14 @@ namespace Elsa.Bpmn.IntegrationTests.Scenarios.HostPort; /// /// /// Every activity's id is the BPMN element id it runs, and its binding ref is that id prefixed, so a process reads -/// the same way in the model and in the assertions. +/// the same way in the model and in the assertions. The processes themselves live in sibling partials, one per BPMN +/// construct family; this file holds the element factories and scope builders they all share. /// -internal static class BpmnTestProcesses +internal static partial class BpmnTestProcesses { - /// An interrupting timer boundary event on a long-running task. - public static BpmnProcess InterruptingTimerBoundary(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("interrupting-timer-boundary") - .StartEvent("start") - .Task("task", bindingRef: BindingRef("task")) - .EndEvent("end") - .BoundaryEvent("timeout", attachedTo: "task", eventDefinition: Timer(), interrupting: true, bindingRef: BindingRef("timeout")) - .Task("onTimeout", bindingRef: BindingRef("onTimeout")) - .EndEvent("timedOut") - .ConnectSequence("start", "task", "end") - .ConnectSequence("timeout", "onTimeout", "timedOut") - .Build(); - - return Scope("scope", definition, Blocking("task", log), Blocking("timeout", log), Immediate("onTimeout", log)); - } - - /// - /// An escalation thrown out of an embedded subprocess, caught by a non-interrupting escalation boundary event on - /// the subprocess. The nested scope keeps running, which is what "non-interrupting" means. - /// - public static BpmnProcess EscalationOutOfSubprocess(BpmnTestLog log) - { - // subFirst runs before anything the parent could be confused with, deliberately: it puts the nested scope's - // handle counter ahead of the parent's. A host that recognised its work by a shared, rewritable key rather than - // by the child activity execution would otherwise be rescued by two independent counters happening to agree. - var body = new BpmnProcessBuilder("subprocess-body") - .StartEvent("subStart") - .Task("subFirst", bindingRef: BindingRef("subFirst")) - .Task("subWork", bindingRef: BindingRef("subWork")) - .IntermediateThrowEvent("subEscalate", Escalation("REVIEW")) - .Task("subMore", bindingRef: BindingRef("subMore")) - .EndEvent("subEnd") - .ConnectSequence("subStart", "subFirst", "subWork", "subEscalate", "subMore", "subEnd") - .Build(); - - var definition = new BpmnProcessBuilder("escalation-out-of-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .BoundaryEvent("escalated", attachedTo: "sub", eventDefinition: Escalation("REVIEW"), interrupting: false) - .Task("notify", bindingRef: BindingRef("notify")) - .EndEvent("notified") - .ConnectSequence("start", "sub", "after", "end") - .ConnectSequence("escalated", "notify", "notified") - .Build(); - - // subMore blocks so the subprocess is still running when the escalation path executes, which is what makes - // "the escalating work is still live" observable rather than merely asserted. - var nested = Scope("sub", body, Immediate("subFirst", log), Blocking("subWork", log), Blocking("subMore", log)); - - return Scope("scope", definition, nested, Immediate("after", log), Immediate("notify", log)); - } - - /// A parallel gateway split and join. - public static BpmnProcess ParallelSplitAndJoin(BpmnTestLog log) => - ParallelSplitAndJoinTopology("parallel-split-and-join", Immediate("left", log), Immediate("right", log), log); - - /// A task that fails, with an error boundary event that catches it. - public static BpmnProcess ErrorBoundaryCaught(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("error-boundary-caught") - .StartEvent("start") - .Task("risky", bindingRef: BindingRef("risky")) - .EndEvent("end") - .BoundaryEvent("oops", attachedTo: "risky", eventDefinition: new BpmnEventDefinition(BpmnEventDefinitionTypes.Error)) - .Task("recover", bindingRef: BindingRef("recover")) - .EndEvent("recovered") - .ConnectSequence("start", "risky", "end") - .ConnectSequence("oops", "recover", "recovered") - .Build(); - - return Scope("scope", definition, Faulting("risky", log), Immediate("recover", log)); - } - - /// - /// A task that fails inside an embedded subprocess with nothing there to catch it, and an error boundary event on - /// the subprocess in the enclosing scope that does. - /// - public static BpmnProcess ErrorPropagatedOutOfSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("failing-subprocess-body") - .StartEvent("subStart") - .Task("subRisky", bindingRef: BindingRef("subRisky")) - .EndEvent("subEnd") - .ConnectSequence("subStart", "subRisky", "subEnd") - .Build(); - - var definition = new BpmnProcessBuilder("error-propagated-out-of-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .BoundaryEvent("subOops", attachedTo: "sub", eventDefinition: new BpmnEventDefinition(BpmnEventDefinitionTypes.Error)) - .Task("subRecover", bindingRef: BindingRef("subRecover")) - .EndEvent("subRecovered") - .ConnectSequence("start", "sub", "after", "end") - .ConnectSequence("subOops", "subRecover", "subRecovered") - .Build(); - - var nested = Scope("sub", body, Faulting("subRisky", log)); - - return Scope("scope", definition, nested, Immediate("after", log), Immediate("subRecover", log)); - } - - /// A task that fails with nothing to catch it. - public static BpmnProcess UncaughtError(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("uncaught-error") - .StartEvent("start") - .Task("risky", bindingRef: BindingRef("risky")) - .EndEvent("end") - .ConnectSequence("start", "risky", "end") - .Build(); - - return Scope("scope", definition, Faulting("risky", log)); - } - - /// - /// A parallel multi-instance task: two concurrent instances of one binding, told apart only by their iteration id. - /// - public static BpmnProcess ParallelMultiInstanceTask(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("parallel-multi-instance-task") - .StartEvent("start") - .Task(BpmnElementTypes.Task, "each", bindingRef: BindingRef("each"), loopCharacteristics: new BpmnLoopCharacteristics(isSequential: false, cardinality: 2)) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "each", "after", "end") - .Build(); - - return Scope("scope", definition, Blocking("each", log), Immediate("after", log)); - } - - /// - /// A sequential multi-instance task: one instance at a time, each blocking until a test finishes it. Used to - /// drive many evaluations of one scope so a persisted blob that is not pruned is observable as unbounded growth. - /// - public static BpmnProcess SequentialMultiInstanceTask(BpmnTestLog log, int cardinality) - { - var definition = new BpmnProcessBuilder("sequential-multi-instance-task") - .StartEvent("start") - .Task(BpmnElementTypes.Task, "each", bindingRef: BindingRef("each"), loopCharacteristics: new BpmnLoopCharacteristics(isSequential: true, cardinality: cardinality)) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "each", "after", "end") - .Build(); - - return Scope("scope", definition, Blocking("each", log), Immediate("after", log)); - } - - /// - /// A parallel split into two branches that both block, and a join. Used to prove a scope suspends with two live - /// units of work outstanding and, once resumed, matches each completion back to its own binding through the - /// rehydrated ledger. - /// - public static BpmnProcess ParallelSplitAndJoinBlocking(BpmnTestLog log) => - ParallelSplitAndJoinTopology("parallel-split-and-join-blocking", Blocking("left", log), Blocking("right", log), log); - - /// - /// The start/split/left/right/join/after/end graph shared by and - /// , parameterised by the work the two branches run. - /// - private static BpmnProcess ParallelSplitAndJoinTopology(string processId, IActivity leftWork, IActivity rightWork, BpmnTestLog log) - { - var definition = new BpmnProcessBuilder(processId) - .StartEvent("start") - .ParallelGateway("split") - .Task("left", bindingRef: BindingRef("left")) - .Task("right", bindingRef: BindingRef("right")) - .ParallelGateway("join") - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "split") - .Connect("split", "left") - .Connect("split", "right") - .Connect("left", "join") - .Connect("right", "join") - .ConnectSequence("join", "after", "end") - .Build(); - - return Scope("scope", definition, leftWork, rightWork, Immediate("after", log)); - } - - /// - /// A collection-mode multi-instance task: one instance per item of a container-scoped variable, which the - /// interpreter reads back through IBpmnVariableReader while it evaluates. - /// - /// - /// Three items rather than two, so the instance count cannot be confused with a declared cardinality. The - /// collection variable is declared on both sides — on the definition, because BpmnGraph.Build refuses a - /// loop naming a variable the process does not declare, and on the activity, because that is where the value - /// actually lives. - /// - public static BpmnProcess CollectionMultiInstanceTask(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("collection-multi-instance-task") - .Variable(CollectionVariableName) - .StartEvent("start") - .Task(BpmnElementTypes.Task, "each", bindingRef: BindingRef("each"), loopCharacteristics: new BpmnLoopCharacteristics(isSequential: false, collectionVariable: CollectionVariableName)) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "each", "after", "end") - .Build(); - - return Scope("scope", definition, [new Variable(CollectionVariableName, ["alpha", "beta", "gamma"])], Immediate("each", log), Immediate("after", log)); - } - - /// - /// A transaction subprocess that cancels itself from the inside, and a cancel boundary event on the transaction - /// that routes the cancellation. - /// - /// - /// The nested scope completes with the Cancelled outcome rather than Done, and the enclosing scope - /// only reaches the boundary path if that outcome survives the trip through the parent's completion callback. - /// Nothing else in the process distinguishes the two: with the outcome dropped the parent simply carries on down - /// the ordinary sequence flow, which is a completion that looks entirely successful. - /// - public static BpmnProcess CancelledTransactionSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("transaction-body") - .Transaction() - .StartEvent("subStart") - .Task("subWork", bindingRef: BindingRef("subWork")) - .EndEvent("subCancelled", null, Cancel()) - .ConnectSequence("subStart", "subWork", "subCancelled") - .Build(); - - var definition = new BpmnProcessBuilder("cancelled-transaction-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub"), isTransaction: true) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .BoundaryEvent("cancelled", attachedTo: "sub", eventDefinition: Cancel()) - .Task("unwind", bindingRef: BindingRef("unwind")) - .EndEvent("unwound") - .ConnectSequence("start", "sub", "after", "end") - .ConnectSequence("cancelled", "unwind", "unwound") - .Build(); - - var nested = Scope("sub", body, Immediate("subWork", log)); - - return Scope("scope", definition, nested, Immediate("after", log), Immediate("unwind", log)); - } - - /// - /// Three bookings, each carrying a compensation boundary event, and a compensate end event that replays the lot. - /// - /// - /// Three rather than two: with two, a handler order that merely happened to be reversed is indistinguishable from - /// one that swapped a pair, and a replay that walked the log forwards would still run every handler. The three - /// handlers bind work like anything else, and nothing in the graph flows into them — the only thing that can run - /// them is the replay. - /// - public static BpmnProcess CompensatedBookings(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("compensated-bookings") - .StartEvent("start") - .Task("bookFlight", bindingRef: BindingRef("bookFlight")) - .Task("bookHotel", bindingRef: BindingRef("bookHotel")) - .Task("bookCar", bindingRef: BindingRef("bookCar")) - .EndEvent("undoEverything", null, Compensation()) - .Element(CompensationBoundary("flightCompensated", attachedTo: "bookFlight", handler: "undoFlight")) - .Element(CompensationBoundary("hotelCompensated", attachedTo: "bookHotel", handler: "undoHotel")) - .Element(CompensationBoundary("carCompensated", attachedTo: "bookCar", handler: "undoCar")) - .Element(CompensationHandler("undoFlight")) - .Element(CompensationHandler("undoHotel")) - .Element(CompensationHandler("undoCar")) - .ConnectSequence("start", "bookFlight", "bookHotel", "bookCar", "undoEverything") - .Build(); - - return Scope( - "scope", - definition, - Immediate("bookFlight", log), - Immediate("bookHotel", log), - Immediate("bookCar", log), - Immediate("undoFlight", log), - Immediate("undoHotel", log), - Immediate("undoCar", log)); - } - - /// - /// The same three bookings, compensated by an intermediate throw event naming one of them in its - /// activityRef, and a task after the throw that its outbound flow reaches once the replay is done. - /// - public static BpmnProcess TargetedCompensation(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("targeted-compensation") - .StartEvent("start") - .Task("bookFlight", bindingRef: BindingRef("bookFlight")) - .Task("bookHotel", bindingRef: BindingRef("bookHotel")) - .Task("bookCar", bindingRef: BindingRef("bookCar")) - .IntermediateThrowEvent("undoHotelOnly", Compensation(activityRef: "bookHotel")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .Element(CompensationBoundary("flightCompensated", attachedTo: "bookFlight", handler: "undoFlight")) - .Element(CompensationBoundary("hotelCompensated", attachedTo: "bookHotel", handler: "undoHotel")) - .Element(CompensationBoundary("carCompensated", attachedTo: "bookCar", handler: "undoCar")) - .Element(CompensationHandler("undoFlight")) - .Element(CompensationHandler("undoHotel")) - .Element(CompensationHandler("undoCar")) - .ConnectSequence("start", "bookFlight", "bookHotel", "bookCar", "undoHotelOnly", "after", "end") - .Build(); - - return Scope( - "scope", - definition, - Immediate("bookFlight", log), - Immediate("bookHotel", log), - Immediate("bookCar", log), - Immediate("undoFlight", log), - Immediate("undoHotel", log), - Immediate("undoCar", log), - Immediate("after", log)); - } - - /// - /// A transaction subprocess that starts a compensation replay on one branch and cancels itself on the other while - /// that replay is still running, so the replay's claimed-but-unrun log entries are torn down mid-run. - /// - /// - /// - /// The shape is what makes the release observable. chargeCard and reserveSeat both complete and - /// register, in that order, so the replay the intermediate throw opens claims both and runs them in reverse: - /// releaseSeat first, which blocks, leaving refundCard claimed and never started. Cancelling the - /// transaction from the other branch stops the replay's coordinating token, which is the only thing in scope that - /// tears a run down mid-flight. - /// - /// - /// fraudCheck blocks so a test decides when the cancellation happens, rather than racing the replay. - /// - /// - public static BpmnProcess CompensationRunCancelledMidReplay(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("mid-replay-cancel-body") - .Transaction() - .StartEvent("subStart") - .ParallelGateway("subSplit") - .Task("chargeCard", bindingRef: BindingRef("chargeCard")) - .Task("reserveSeat", bindingRef: BindingRef("reserveSeat")) - .IntermediateThrowEvent("rollBack", Compensation()) - .EndEvent("rolledBack") - .Task("fraudCheck", bindingRef: BindingRef("fraudCheck")) - .EndEvent("subCancelled", null, Cancel()) - .Element(CompensationBoundary("cardCompensated", attachedTo: "chargeCard", handler: "refundCard")) - .Element(CompensationBoundary("seatCompensated", attachedTo: "reserveSeat", handler: "releaseSeat")) - .Element(CompensationHandler("refundCard")) - .Element(CompensationHandler("releaseSeat")) - .ConnectSequence("subStart", "subSplit") - .Connect("subSplit", "chargeCard") - .ConnectSequence("chargeCard", "reserveSeat", "rollBack", "rolledBack") - .Connect("subSplit", "fraudCheck") - .ConnectSequence("fraudCheck", "subCancelled") - .Build(); - - var definition = new BpmnProcessBuilder("mid-replay-cancel") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub"), isTransaction: true) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .BoundaryEvent("cancelled", attachedTo: "sub", eventDefinition: Cancel()) - .Task("unwind", bindingRef: BindingRef("unwind")) - .EndEvent("unwound") - .ConnectSequence("start", "sub", "after", "end") - .ConnectSequence("cancelled", "unwind", "unwound") - .Build(); - - var nested = Scope( - "sub", - body, - Immediate("chargeCard", log), - Immediate("reserveSeat", log), - Blocking("fraudCheck", log), - Immediate("refundCard", log), - Blocking("releaseSeat", log)); - - return Scope("scope", definition, nested, Immediate("after", log), Immediate("unwind", log)); - } - - /// - /// A transaction subprocess that cancels itself from the inside, with nothing on the enclosing scope to route the - /// cancellation. - /// - /// - /// The same shape as minus the cancel boundary event. Graph - /// validation cannot see into the nested definition to know a cancel end event is in there, so an unroutable - /// cancellation is an execution-time rule: the enclosing scope faults rather than treating the transaction as an - /// ordinary completion and carrying on down the sequence flow. - /// - public static BpmnProcess CancelledTransactionWithoutCancelBoundary(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("unroutable-cancel-body") - .Transaction() - .StartEvent("subStart") - .Task("subWork", bindingRef: BindingRef("subWork")) - .EndEvent("subCancelled", null, Cancel()) - .ConnectSequence("subStart", "subWork", "subCancelled") - .Build(); - - var definition = new BpmnProcessBuilder("unroutable-cancel") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub"), isTransaction: true) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "sub", "after", "end") - .Build(); - - var nested = Scope("sub", body, Immediate("subWork", log)); - - return Scope("scope", definition, nested, Immediate("after", log)); - } - - /// - /// A compensation log inside an embedded subprocess, and a second one in the enclosing scope that compensates the - /// subprocess itself. - /// - /// - /// Two logs, one per scope, and neither can see the other: the body replays its own two handlers before it - /// completes, and the enclosing scope registers exactly one entry — the subprocess's own successful completion, - /// which its attached compensation boundary makes compensable — and replays that. - /// - public static BpmnProcess CompensationInsideSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("compensating-subprocess-body") - .StartEvent("subStart") - .Task("subCharge", bindingRef: BindingRef("subCharge")) - .Task("subShip", bindingRef: BindingRef("subShip")) - .EndEvent("subUndo", null, Compensation()) - .Element(CompensationBoundary("subChargeCompensated", attachedTo: "subCharge", handler: "subRefund")) - .Element(CompensationBoundary("subShipCompensated", attachedTo: "subShip", handler: "subRecall")) - .Element(CompensationHandler("subRefund")) - .Element(CompensationHandler("subRecall")) - .ConnectSequence("subStart", "subCharge", "subShip", "subUndo") - .Build(); - - var definition = new BpmnProcessBuilder("compensation-inside-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .EndEvent("undoOuter", null, Compensation()) - .Element(CompensationBoundary("subCompensated", attachedTo: "sub", handler: "undoSub")) - .Element(CompensationHandler("undoSub")) - .ConnectSequence("start", "sub", "undoOuter") - .Build(); - - var nested = Scope( - "sub", - body, - Immediate("subCharge", log), - Immediate("subShip", log), - Immediate("subRefund", log), - Immediate("subRecall", log)); - - return Scope("scope", definition, nested, Immediate("undoSub", log)); - } - - /// - /// A task that fails, with a dormant error-triggered event subprocess in the same scope to catch it. - /// - /// - /// An error event subprocess arms nothing: it rides the same FaultSignal seam an error boundary event does, - /// and the only thing that distinguishes it here is where the recovery work runs — inside a nested scope of its - /// own, seeded at the body's error start event, rather than on an outbound flow of the enclosing graph. - /// - public static BpmnProcess ErrorEventSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("error-event-subprocess-body") - .Element(EventSubprocessStart("errStart", Error())) - .Task("handleError", bindingRef: BindingRef("handleError")) - .EndEvent("errEnd") - .ConnectSequence("errStart", "handleError", "errEnd") - .Build(); - - var definition = new BpmnProcessBuilder("error-event-subprocess") - .StartEvent("start") - .Task("risky", bindingRef: BindingRef("risky")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .Element(EventSubprocess("evtSub")) - .ConnectSequence("start", "risky", "after", "end") - .Build(); - - return Scope("scope", definition, Faulting("risky", log), Immediate("after", log), Scope("evtSub", body, Immediate("handleError", log))); - } - - /// - /// An escalation thrown out of an embedded subprocess, caught by a non-interrupting escalation-triggered event - /// subprocess on the enclosing scope rather than by a boundary event on the subprocess. - /// - /// - /// Non-interrupting, so the escalating subprocess keeps running and nothing in the scope is torn down. That is - /// also what makes "the scope-level catcher fired" distinguishable from "the subprocess was stopped": with an - /// interrupting catcher the two are the same observation. - /// - public static BpmnProcess EscalationEventSubprocessOutOfSubprocess(BpmnTestLog log) - { - var subBody = new BpmnProcessBuilder("escalating-subprocess-body") - .StartEvent("subStart") - .Task("subWork", bindingRef: BindingRef("subWork")) - .IntermediateThrowEvent("subEscalate", Escalation("REVIEW")) - .Task("subMore", bindingRef: BindingRef("subMore")) - .EndEvent("subEnd") - .ConnectSequence("subStart", "subWork", "subEscalate", "subMore", "subEnd") - .Build(); - - var handlerBody = new BpmnProcessBuilder("escalation-event-subprocess-body") - .Element(EventSubprocessStart("escStart", Escalation("REVIEW"), interrupting: false)) - .Task("handleEscalation", bindingRef: BindingRef("handleEscalation")) - .EndEvent("escEnd") - .ConnectSequence("escStart", "handleEscalation", "escEnd") - .Build(); - - var definition = new BpmnProcessBuilder("escalation-event-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .Element(EventSubprocess("evtSub")) - .ConnectSequence("start", "sub", "after", "end") - .Build(); - - var nested = Scope("sub", subBody, Blocking("subWork", log), Blocking("subMore", log)); - - return Scope("scope", definition, nested, Immediate("after", log), Scope("evtSub", handlerBody, Immediate("handleEscalation", log))); - } - - /// - /// A non-interrupting message-triggered event subprocess: a listener armed at scope start, and a body that runs - /// each time the listener fires while the scope's own long-running work is still going. - /// - /// - /// - /// The listener is the second binding channel — listenerBindingRef — and is bound in the same - /// WorkBindings map as everything else. It stands in for a real message wait: blocking work a test - /// finishes, which is exactly what "the trigger fired" means to the host. - /// - /// - /// work blocks so the scope stays open across the fires and so that when it finally completes, the armed - /// listener is a running activity rather than a scheduled-but-not-yet-invoked one — the second of which - /// this host cannot withdraw at all. - /// - /// - public static BpmnProcess MessageEventSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("message-event-subprocess-body") - .Element(EventSubprocessStart("msgStart", Message("nudge"), interrupting: false)) - .Task("handleNudge", bindingRef: BindingRef("handleNudge")) - .EndEvent("msgEnd") - .ConnectSequence("msgStart", "handleNudge", "msgEnd") - .Build(); - - var definition = new BpmnProcessBuilder("message-event-subprocess") - .StartEvent("start") - .Task("work", bindingRef: BindingRef("work")) - .EndEvent("end") - .Element(EventSubprocess("evtSub", listenerBindingRef: BindingRef("nudgeListener"))) - .ConnectSequence("start", "work", "end") - .Build(); - - return Scope( - "scope", - definition, - Blocking("work", log), - Blocking("nudgeListener", log), - Scope("evtSub", body, Immediate("handleNudge", log))); - } - - /// - /// The same message-triggered event subprocess, but inside an embedded subprocess that completes while the - /// enclosing scope carries on — so a listener that outlived the scope that armed it is distinguishable from one - /// that merely outlived the workflow. - /// - /// - /// At the root, "the armed work does not survive the scope" and "does not survive the workflow" are the same - /// observation, and Elsa tears a finished workflow's children down regardless. Here the workflow keeps running - /// after the scope that armed the listener has completed, which is the only shape in which a listener left behind - /// is a listener something could still resume into. - /// - public static BpmnProcess NestedMessageEventSubprocess(BpmnTestLog log) - { - var handlerBody = new BpmnProcessBuilder("nested-message-event-subprocess-body") - .Element(EventSubprocessStart("msgStart", Message("nudge"), interrupting: false)) - .Task("handleNudge", bindingRef: BindingRef("handleNudge")) - .EndEvent("msgEnd") - .ConnectSequence("msgStart", "handleNudge", "msgEnd") - .Build(); - - var subBody = new BpmnProcessBuilder("listening-subprocess-body") - .StartEvent("subStart") - .Task("subWork", bindingRef: BindingRef("subWork")) - .EndEvent("subEnd") - .Element(EventSubprocess("evtSub", listenerBindingRef: BindingRef("nudgeListener"))) - .ConnectSequence("subStart", "subWork", "subEnd") - .Build(); - - var definition = new BpmnProcessBuilder("nested-message-event-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "sub", "after", "end") - .Build(); - - var nested = Scope( - "sub", - subBody, - Blocking("subWork", log), - Blocking("nudgeListener", log), - Scope("evtSub", handlerBody, Immediate("handleNudge", log))); - - return Scope("scope", definition, nested, Immediate("after", log)); - } - - /// - /// An error-triggered event subprocess whose body runs an ordinary embedded subprocess of its own, so the - /// start-element hint has both a place to arrive and a place it must not reach. - /// - /// - /// - /// The body's only start event is event-defined, which is what makes the hint's arrival observable rather than - /// merely asserted: seeded from the hint the body runs, and seeded as an ordinary direct invocation it faults - /// deterministically with bpmn.start.none-available, because there is no none start event to begin at. - /// - /// - /// The nested inner subprocess is the other direction. Its own invocation carries an ordinary scheduling - /// cause, so the hint must not be inherited: were it, the inner process would be seeded at an element it does not - /// declare and fault with bpmn.start.unresolved-hint instead of starting at its own none start event. - /// - /// - public static BpmnProcess EventSubprocessBodyWithNestedSubprocess(BpmnTestLog log) - { - var innerBody = new BpmnProcessBuilder("event-subprocess-inner-body") - .StartEvent("innerStart") - .Task("innerOnly", bindingRef: BindingRef("innerOnly")) - .EndEvent("innerEnd") - .ConnectSequence("innerStart", "innerOnly", "innerEnd") - .Build(); - - var body = new BpmnProcessBuilder("hinted-event-subprocess-body") - .Element(EventSubprocessStart("errStart", Error())) - .Task("handleError", bindingRef: BindingRef("handleError")) - .SubProcess("inner", bindingRef: BindingRef("inner")) - .EndEvent("errEnd") - .ConnectSequence("errStart", "handleError", "inner", "errEnd") - .Build(); - - var definition = new BpmnProcessBuilder("event-subprocess-start-hint") - .StartEvent("start") - .Task("risky", bindingRef: BindingRef("risky")) - .EndEvent("end") - .Element(EventSubprocess("evtSub")) - .ConnectSequence("start", "risky", "end") - .Build(); - - var handler = Scope("evtSub", body, Immediate("handleError", log), Scope("inner", innerBody, Immediate("innerOnly", log))); - - return Scope("scope", definition, Faulting("risky", log), handler); - } - - /// An event subprocess whose body declares two start events, which the library refuses. - public static BpmnProcess EventSubprocessBodyWithTwoStartEvents(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("two-start-event-subprocess-body") - .Element(EventSubprocessStart("errStart", Error())) - .StartEvent("alsoStart") - .Task("handleError", bindingRef: BindingRef("handleError")) - .EndEvent("errEnd") - .ConnectSequence("errStart", "handleError", "errEnd") - .Connect("alsoStart", "handleError") - .Build(); - - return RefusedEventSubprocessScope("two-start-events", log, ("evtSub", body, "handleError")); - } - - /// Two error-triggered event subprocesses in one scope, which the library refuses. - public static BpmnProcess TwoErrorEventSubprocesses(BpmnTestLog log) - { - BpmnProcessDefinition Body(string prefix) => new BpmnProcessBuilder($"{prefix}-error-event-subprocess-body") - .Element(EventSubprocessStart($"{prefix}Start", Error())) - .Task($"{prefix}Handle", bindingRef: BindingRef($"{prefix}Handle")) - .EndEvent($"{prefix}End") - .ConnectSequence($"{prefix}Start", $"{prefix}Handle", $"{prefix}End") - .Build(); - - return RefusedEventSubprocessScope( - "two-error-event-subprocesses", - log, - ("evtSubA", Body("first"), "firstHandle"), - ("evtSubB", Body("second"), "secondHandle")); - } - - /// Two code-less catch-all escalation-triggered event subprocesses in one scope, which the library refuses. - public static BpmnProcess TwoCatchAllEscalationEventSubprocesses(BpmnTestLog log) - { - BpmnProcessDefinition Body(string prefix) => new BpmnProcessBuilder($"{prefix}-escalation-event-subprocess-body") - .Element(EventSubprocessStart($"{prefix}Start", Escalation(), interrupting: false)) - .Task($"{prefix}Handle", bindingRef: BindingRef($"{prefix}Handle")) - .EndEvent($"{prefix}End") - .ConnectSequence($"{prefix}Start", $"{prefix}Handle", $"{prefix}End") - .Build(); - - return RefusedEventSubprocessScope( - "two-catch-all-escalation-event-subprocesses", - log, - ("evtSubA", Body("first"), "firstHandle"), - ("evtSubB", Body("second"), "secondHandle")); - } - - /// A non-interrupting error-triggered event subprocess, which is not legal BPMN and which the library refuses. - public static BpmnProcess NonInterruptingErrorEventSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("non-interrupting-error-event-subprocess-body") - .Element(EventSubprocessStart("errStart", Error(), interrupting: false)) - .Task("handleError", bindingRef: BindingRef("handleError")) - .EndEvent("errEnd") - .ConnectSequence("errStart", "handleError", "errEnd") - .Build(); - - return RefusedEventSubprocessScope("non-interrupting-error-event-subprocess", log, ("evtSub", body, "handleError")); - } - - /// - /// The start/only/end graph the refusal processes share, carrying the event subprocesses whose declaration - /// the library refuses. Nothing in it ever runs: the refusal is raised when the scope builds its graph, which is - /// before any work is started. - /// - private static BpmnProcess RefusedEventSubprocessScope(string processId, BpmnTestLog log, params (string ElementId, BpmnProcessDefinition Body, string HandlerId)[] eventSubprocesses) - { - var builder = new BpmnProcessBuilder(processId) - .StartEvent("start") - .Task("only", bindingRef: BindingRef("only")) - .EndEvent("end") - .ConnectSequence("start", "only", "end"); - - foreach (var eventSubprocess in eventSubprocesses) - builder = builder.Element(EventSubprocess(eventSubprocess.ElementId)); - - var work = new List { Immediate("only", log) }; - - work.AddRange(eventSubprocesses.Select(eventSubprocess => Scope(eventSubprocess.ElementId, eventSubprocess.Body, Immediate(eventSubprocess.HandlerId, log)))); - - return Scope("scope", builder.Build(), work.ToArray()); - } - - /// An embedded subprocess with one task in it, and one task after it in the enclosing scope. - public static BpmnProcess NestedSubprocess(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("nested-subprocess-body") - .StartEvent("subStart") - .Task("subOnly", bindingRef: BindingRef("subOnly")) - .EndEvent("subEnd") - .ConnectSequence("subStart", "subOnly", "subEnd") - .Build(); - - var definition = new BpmnProcessBuilder("nested-subprocess") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "sub", "after", "end") - .Build(); - - return Scope("scope", definition, Scope("sub", body, Immediate("subOnly", log)), Immediate("after", log)); - } - - /// - /// A parallel split and join, blocking on both branches, nested inside an embedded subprocess. Used to prove - /// a nested scope's own ledger -- not just a root scope's -- matches each completion back to its - /// binding after a round trip through Elsa's own serializer. - /// - public static BpmnProcess NestedParallelSplitAndJoinBlocking(BpmnTestLog log) - { - var body = new BpmnProcessBuilder("nested-parallel-split-and-join-body") - .StartEvent("subStart") - .ParallelGateway("subSplit") - .Task("subLeft", bindingRef: BindingRef("subLeft")) - .Task("subRight", bindingRef: BindingRef("subRight")) - .ParallelGateway("subJoin") - .Task("subAfter", bindingRef: BindingRef("subAfter")) - .EndEvent("subEnd") - .ConnectSequence("subStart", "subSplit") - .Connect("subSplit", "subLeft") - .Connect("subSplit", "subRight") - .Connect("subLeft", "subJoin") - .Connect("subRight", "subJoin") - .ConnectSequence("subJoin", "subAfter", "subEnd") - .Build(); - - var definition = new BpmnProcessBuilder("nested-parallel-split-and-join-blocking") - .StartEvent("start") - .SubProcess("sub", bindingRef: BindingRef("sub")) - .Task("after", bindingRef: BindingRef("after")) - .EndEvent("end") - .ConnectSequence("start", "sub", "after", "end") - .Build(); - - var nested = Scope("sub", body, Blocking("subLeft", log), Blocking("subRight", log), Immediate("subAfter", log)); - - return Scope("scope", definition, nested, Immediate("after", log)); - } - - /// A linear process: one task between a start and an end event. - public static BpmnProcess LinearTask(BpmnTestLog log) - { - var definition = new BpmnProcessBuilder("linear-task") - .StartEvent("start") - .Task("only", bindingRef: BindingRef("only")) - .EndEvent("end") - .ConnectSequence("start", "only", "end") - .Build(); - - return Scope("scope", definition, Immediate("only", log)); - } - /// The binding ref the given element's work is declared under. public static string BindingRef(string elementId) => $"node-{elementId}"; - /// The name of the variable loops over. - public const string CollectionVariableName = "items"; - private static BpmnEventDefinition Timer() => new(BpmnEventDefinitionTypes.Timer); private static BpmnEventDefinition Cancel() => new(BpmnEventDefinitionTypes.Cancel);