Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- ============================================================================
-- Bug #320: DESCRIBE emitted default anchor fragments redundantly
-- ============================================================================
--
-- Symptom (before fix):
-- `describe microflow` printed `@anchor` lines for every activity, even
-- when every from/to side matched the MDL default for that flow shape:
-- - regular sequence flows default to `from: right, to: left`
-- - true branches of IF default to `from: right, to: left`
-- - false branches of IF default to `from: bottom, to: top`
-- Output looked like
-- log info node 'X' 'a';
-- @anchor(from: right, to: left) -- noise: this is the default
-- log info node 'X' 'b';
-- producing verbose, non-author-friendly DESCRIBE output and noisy
-- `mxcli diff-local` runs after a roundtrip.
--
-- After fix:
-- `emitAnchorAnnotation` and `emitSplitAnchorAnnotation` (in
-- cmd_microflows_show_helpers.go) suppress fragments that match the
-- default and skip the entire annotation when all sides are defaults.
-- Non-default sides (e.g. `to: top` on a return) are still emitted.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl -p app.mpr
--
-- mxcli -p app.mpr -c "describe microflow BugTest320.MF_LinearDefaults"
-- The output must contain NO `@anchor` line — every flow uses defaults.
--
-- mxcli -p app.mpr -c "describe microflow BugTest320.MF_NonDefaultReturn"
-- The output must keep `@anchor(to: top)` on the return statement.
-- ============================================================================

create module BugTest320;

-- Linear flow, all defaults — describe must emit zero @anchor lines.
create microflow BugTest320.MF_LinearDefaults ()
begin
log info node 'BugTest320' 'a';
log info node 'BugTest320' 'b';
log info node 'BugTest320' 'c';
end;
/

-- One non-default `to: top` on the return — describe must keep that fragment.
create microflow BugTest320.MF_NonDefaultReturn (
$value: integer
)
returns boolean as $ok
begin
log info node 'BugTest320' 'check';
@anchor(to: top)
return $value > 0;
end;
/
32 changes: 32 additions & 0 deletions mdl/executor/cmd_microflows_describe_anchor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ func TestEmitAnchorAnnotation_FromAndTo(t *testing.T) {
}
}

func TestEmitAnchorAnnotation_OmitsDefaultRightToLeft(t *testing.T) {
activity := &microflows.ActionActivity{
BaseActivity: microflows.BaseActivity{
BaseMicroflowObject: microflows.BaseMicroflowObject{
BaseElement: model.BaseElement{ID: "act-default"},
},
},
}
incoming := &microflows.SequenceFlow{
DestinationID: "act-default",
DestinationConnectionIndex: AnchorLeft,
}
outgoing := &microflows.SequenceFlow{
OriginID: "act-default",
OriginConnectionIndex: AnchorRight,
}

flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{
"act-default": {outgoing},
}
flowsByDest := map[model.ID][]*microflows.SequenceFlow{
"act-default": {incoming},
}

var lines []string
emitAnchorAnnotation(activity, flowsByOrigin, flowsByDest, &lines, "")

if len(lines) != 0 {
t.Fatalf("expected default anchor line to be omitted, got %v", lines)
}
}

func TestEmitAnchorAnnotation_NoFlowsSkipsEmission(t *testing.T) {
activity := &microflows.ActionActivity{
BaseActivity: microflows.BaseActivity{
Expand Down
6 changes: 3 additions & 3 deletions mdl/executor/cmd_microflows_describe_concurrent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestFormatMicroflowActivities_Concurrent_NoRace(t *testing.T) {
// Build two distinct microflows whose activities are anchored to
// different sides. If the two describe calls share state, one will
// emit the other's anchor keyword.
mfA := mkRaceMicroflow("mfa-start", "mfa-log", "mfa-end", AnchorRight)
mfA := mkRaceMicroflow("mfa-start", "mfa-log", "mfa-end", AnchorTop)
mfB := mkRaceMicroflow("mfb-start", "mfb-log", "mfb-end", AnchorBottom)

e := newTestExecutor()
Expand All @@ -50,8 +50,8 @@ func TestFormatMicroflowActivities_Concurrent_NoRace(t *testing.T) {
}
wg.Wait()

wantA := "@anchor(from: right, to: left)"
wantB := "@anchor(from: bottom, to: left)"
wantA := "@anchor(from: top)"
wantB := "@anchor(from: bottom)"
for i, got := range resultsA {
if !strings.Contains(got, wantA) {
t.Errorf("worker %d (A) missing %q in output:\n%s", i, wantA, got)
Expand Down
23 changes: 18 additions & 5 deletions mdl/executor/cmd_microflows_show_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ func emitAnchorAnnotation(
return
}
var parts []string
if from != "" {
if from != "" && from != "right" {
parts = append(parts, "from: "+from)
}
if to != "" {
if to != "" && to != "left" {
parts = append(parts, "to: "+to)
}
if len(parts) == 0 {
return
}
*lines = append(*lines, indentStr+fmt.Sprintf("@anchor(%s)", strings.Join(parts, ", ")))
}

Expand Down Expand Up @@ -176,13 +179,13 @@ func emitSplitAnchorAnnotation(
}

var parts []string
if inTo != "" {
if inTo != "" && inTo != "left" {
parts = append(parts, "to: "+inTo)
}
if p := branchAnchorFragment("true", trueFrom, trueTo); p != "" {
if p := branchAnchorFragmentWithDefaults("true", trueFrom, trueTo, "right", "left"); p != "" {
parts = append(parts, p)
}
if p := branchAnchorFragment("false", falseFrom, falseTo); p != "" {
if p := branchAnchorFragmentWithDefaults("false", falseFrom, falseTo, "bottom", "top"); p != "" {
parts = append(parts, p)
}
if len(parts) == 0 {
Expand All @@ -207,6 +210,16 @@ func branchAnchorFragment(label, from, to string) string {
return fmt.Sprintf("%s: (%s)", label, strings.Join(inner, ", "))
}

func branchAnchorFragmentWithDefaults(label, from, to, defaultFrom, defaultTo string) string {
if from == defaultFrom {
from = ""
}
if to == defaultTo {
to = ""
}
return branchAnchorFragment(label, from, to)
}

// emitLoopAnchorAnnotation emits the loop form of @anchor for a LoopedActivity.
// A LoopedActivity has up to four flows worth describing:
// - the incoming flow from the previous activity (normal `to:`)
Expand Down
61 changes: 45 additions & 16 deletions mdl/executor/cmd_microflows_split_incoming_anchor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,16 @@ func TestEmitSplitAnchor_EmitsBranchAnchors(t *testing.T) {

trueFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorRight,
DestinationConnectionIndex: AnchorLeft,
OriginConnectionIndex: AnchorTop,
DestinationConnectionIndex: AnchorTop,
CaseValue: microflows.EnumerationCase{
Value: "true",
},
}
falseFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorBottom,
DestinationConnectionIndex: AnchorTop,
OriginConnectionIndex: AnchorLeft,
DestinationConnectionIndex: AnchorRight,
CaseValue: microflows.EnumerationCase{
Value: "false",
},
Expand All @@ -84,15 +84,44 @@ func TestEmitSplitAnchor_EmitsBranchAnchors(t *testing.T) {
out := lines[0]

for _, want := range []string{
"true: (from: right, to: left)",
"false: (from: bottom, to: top)",
"true: (from: top, to: top)",
"false: (from: left, to: right)",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q\nfull: %s", want, out)
}
}
}

func TestEmitSplitAnchor_OmitsDefaultBranchAnchors(t *testing.T) {
splitID := model.ID("split-defaults")
split := &microflows.ExclusiveSplit{}
split.ID = splitID

trueFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorRight,
DestinationConnectionIndex: AnchorLeft,
CaseValue: &microflows.ExpressionCase{Expression: "true"},
}
falseFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorBottom,
DestinationConnectionIndex: AnchorTop,
CaseValue: &microflows.ExpressionCase{Expression: "false"},
}
flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{
splitID: {trueFlow, falseFlow},
}

var lines []string
emitAnchorAnnotation(split, flowsByOrigin, nil, &lines, "")

if len(lines) != 0 {
t.Fatalf("expected default branch anchor line to be omitted, got %v", lines)
}
}

func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) {
// Mendix splits often use ExpressionCase (Expression == "true" / "false")
// instead of EnumerationCase. The anchor emission must identify the
Expand All @@ -105,16 +134,16 @@ func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) {

trueFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorRight,
DestinationConnectionIndex: AnchorLeft,
OriginConnectionIndex: AnchorTop,
DestinationConnectionIndex: AnchorTop,
CaseValue: &microflows.ExpressionCase{
Expression: "true",
},
}
falseFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorBottom,
DestinationConnectionIndex: AnchorTop,
OriginConnectionIndex: AnchorLeft,
DestinationConnectionIndex: AnchorRight,
CaseValue: &microflows.ExpressionCase{
Expression: "false",
},
Expand All @@ -131,8 +160,8 @@ func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) {
}
out := lines[0]
for _, want := range []string{
"true: (from: right, to: left)",
"false: (from: bottom, to: top)",
"true: (from: top, to: top)",
"false: (from: left, to: right)",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q\nfull: %s", want, out)
Expand All @@ -155,8 +184,8 @@ func TestEmitSplitAnchor_SupportsBooleanCase(t *testing.T) {
}
falseFlow := &microflows.SequenceFlow{
OriginID: splitID,
OriginConnectionIndex: AnchorBottom,
DestinationConnectionIndex: AnchorTop,
OriginConnectionIndex: AnchorLeft,
DestinationConnectionIndex: AnchorRight,
CaseValue: &microflows.BooleanCase{Value: false},
}
flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{
Expand All @@ -171,8 +200,8 @@ func TestEmitSplitAnchor_SupportsBooleanCase(t *testing.T) {
}
out := lines[0]
for _, want := range []string{
"true: (from: top, to: left)",
"false: (from: bottom, to: top)",
"true: (from: top)",
"false: (from: left, to: right)",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q\nfull: %s", want, out)
Expand Down