diff --git a/mdl/executor/cmd_microflows_show_helpers.go b/mdl/executor/cmd_microflows_show_helpers.go index 264ce323..82eaf94f 100644 --- a/mdl/executor/cmd_microflows_show_helpers.go +++ b/mdl/executor/cmd_microflows_show_helpers.go @@ -724,18 +724,20 @@ func traverseFlow( traverseFlowUntilMerge(ctx, trueFlow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget) } - // Emit the ELSE branch only if it has statements. When the false - // flow jumps straight to the merge (the MDL was `if X then ... end if` - // with no else), emitting `else` with no body produces an empty - // branch that normalizes away on re-parse. - falseHasBody := falseFlow != nil && falseFlow.DestinationID != mergeID - if falseHasBody { + if falseFlow != nil { + elseLineIdx := len(*lines) *lines = append(*lines, indentStr+"else") visitedFalseBranch := make(map[model.ID]bool) for id := range visited { visitedFalseBranch[id] = true } traverseFlowUntilMerge(ctx, falseFlow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visitedFalseBranch, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget) + // Remove empty else block. A false branch can point at a + // continuation already emitted through the true branch, so checking + // only falseFlow.DestinationID != mergeID is not enough. + if len(*lines) == elseLineIdx+1 { + *lines = (*lines)[:elseLineIdx] + } } *lines = append(*lines, indentStr+"end if;") diff --git a/mdl/executor/cmd_microflows_traverse_test.go b/mdl/executor/cmd_microflows_traverse_test.go index 9585fd86..f9f6167a 100644 --- a/mdl/executor/cmd_microflows_traverse_test.go +++ b/mdl/executor/cmd_microflows_traverse_test.go @@ -561,6 +561,63 @@ func TestTraverseFlow_SequentialIfWithoutElseKeepsContinuationOutsideFirstIf(t * } } +func TestTraverseFlow_TopLevelIfRemovesEmptyElseAfterVisitedContinuation(t *testing.T) { + e := newTestExecutor() + + logAction := func(id, message string) *microflows.ActionActivity { + return µflows.ActionActivity{ + BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj(id)}, + Action: µflows.LogMessageAction{ + LogLevel: "Info", + LogNodeName: "'Synthetic'", + MessageTemplate: &model.Text{Translations: map[string]string{"en_US": message}}, + }, + } + } + + activityMap := map[model.ID]microflows.MicroflowObject{ + mkID("start"): µflows.StartEvent{BaseMicroflowObject: mkObj("start")}, + mkID("outer_split"): µflows.ExclusiveSplit{ + BaseMicroflowObject: mkObj("outer_split"), + SplitCondition: µflows.ExpressionSplitCondition{Expression: "$UseNestedPath"}, + }, + mkID("inner_split"): µflows.ExclusiveSplit{ + BaseMicroflowObject: mkObj("inner_split"), + SplitCondition: µflows.ExpressionSplitCondition{Expression: "$HasContinuation"}, + }, + mkID("inner_return"): µflows.EndEvent{BaseMicroflowObject: mkObj("inner_return")}, + mkID("tail_log"): logAction("tail_log", "shared tail"), + mkID("end"): µflows.EndEvent{BaseMicroflowObject: mkObj("end")}, + } + flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ + mkID("start"): {mkFlow("start", "outer_split")}, + mkID("outer_split"): { + mkBranchFlow("outer_split", "inner_split", µflows.ExpressionCase{Expression: "true"}), + mkBranchFlow("outer_split", "tail_log", µflows.ExpressionCase{Expression: "false"}), + }, + mkID("inner_split"): { + mkBranchFlow("inner_split", "tail_log", µflows.ExpressionCase{Expression: "true"}), + mkBranchFlow("inner_split", "inner_return", µflows.ExpressionCase{Expression: "false"}), + }, + mkID("tail_log"): {mkFlow("tail_log", "end")}, + } + + splitMergeMap := map[model.ID]model.ID{ + mkID("outer_split"): mkID("end"), + mkID("inner_split"): mkID("tail_log"), + } + var lines []string + e.traverseFlow(mkID("start"), activityMap, flowsByOrigin, splitMergeMap, make(map[model.ID]bool), nil, nil, &lines, 0, nil, 0, nil) + + out := strings.Join(lines, "\n") + if strings.Contains(out, "\nelse\nend if;") { + t.Fatalf("empty top-level else should be removed:\n%s", out) + } + if got := strings.Count(out, "shared tail"); got != 1 { + t.Fatalf("shared tail emitted %d times, want once:\n%s", got, out) + } +} + func TestTraverseFlow_GuardBranchWithMultipleActivitiesKeepsContinuationOutsideElse(t *testing.T) { e := newTestExecutor()