Skip to content

Commit 49f3400

Browse files
authored
Merge pull request #321 from hjotha/submit/describer-suppress-default-anchor-fragments
fix: suppress default anchor fragments in describe
2 parents ebac593 + 2041812 commit 49f3400

7 files changed

Lines changed: 464 additions & 34 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
-- ============================================================================
2+
-- Bug #320: DESCRIBE emitted default anchor fragments redundantly
3+
-- ============================================================================
4+
--
5+
-- Symptom (before fix):
6+
-- `describe microflow` printed `@anchor` lines for every activity, even
7+
-- when every from/to side matched the MDL default for that flow shape:
8+
-- - regular sequence flows default to `from: right, to: left`
9+
-- - true branches of IF default to `from: right, to: left`
10+
-- - false branches of IF default to `from: bottom, to: top`
11+
-- Output looked like
12+
-- log info node 'X' 'a';
13+
-- @anchor(from: right, to: left) -- noise: this is the default
14+
-- log info node 'X' 'b';
15+
-- producing verbose, non-author-friendly DESCRIBE output and noisy
16+
-- `mxcli diff-local` runs after a roundtrip.
17+
--
18+
-- After fix:
19+
-- `emitAnchorAnnotation` and `emitSplitAnchorAnnotation` (in
20+
-- cmd_microflows_show_helpers.go) suppress fragments that match the
21+
-- default and skip the entire annotation when all sides are defaults.
22+
-- Non-default sides (e.g. `to: top` on a return) are still emitted.
23+
--
24+
-- Usage:
25+
-- mxcli exec mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl -p app.mpr
26+
--
27+
-- mxcli -p app.mpr -c "describe microflow BugTest320.MF_LinearDefaults"
28+
-- The output must contain NO `@anchor` line — every flow uses defaults.
29+
--
30+
-- mxcli -p app.mpr -c "describe microflow BugTest320.MF_NonDefaultReturn"
31+
-- The output must keep `@anchor(to: top)` on the return statement.
32+
-- ============================================================================
33+
34+
create module BugTest320;
35+
36+
-- Linear flow, all defaults — describe must emit zero @anchor lines.
37+
create microflow BugTest320.MF_LinearDefaults ()
38+
begin
39+
log info node 'BugTest320' 'a';
40+
log info node 'BugTest320' 'b';
41+
log info node 'BugTest320' 'c';
42+
end;
43+
/
44+
45+
-- One non-default `to: top` on the return — describe must keep that fragment.
46+
create microflow BugTest320.MF_NonDefaultReturn (
47+
$value: integer
48+
)
49+
returns boolean as $ok
50+
begin
51+
log info node 'BugTest320' 'check';
52+
@anchor(to: top)
53+
return $value > 0;
54+
end;
55+
/

mdl/executor/cmd_microflows_anchor_if_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
package executor
1111

1212
import (
13+
"strings"
1314
"testing"
1415

1516
"github.com/mendixlabs/mxcli/mdl/ast"
17+
"github.com/mendixlabs/mxcli/model"
18+
"github.com/mendixlabs/mxcli/sdk/microflows"
1619
)
1720

1821
// buildWithAnchors is a test helper that builds the flow graph for a simple
@@ -163,6 +166,48 @@ func TestBuilder_AnchorTrueBranchTo_EmptyThenIfWithElse(t *testing.T) {
163166
}
164167
}
165168

169+
func TestDescribe_FalseBranchFromTop_IfWithoutElseIsDefault(t *testing.T) {
170+
body := []ast.MicroflowStatement{
171+
&ast.IfStmt{
172+
Condition: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true},
173+
ThenBody: []ast.MicroflowStatement{
174+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "inside"}},
175+
},
176+
Annotations: &ast.ActivityAnnotations{
177+
FalseBranchAnchor: &ast.FlowAnchors{From: ast.AnchorSideTop, To: ast.AnchorSideUnset},
178+
},
179+
},
180+
&ast.ReturnStmt{},
181+
}
182+
183+
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing}
184+
oc := fb.buildFlowGraph(body, nil)
185+
186+
var split microflows.MicroflowObject
187+
for _, obj := range oc.Objects {
188+
if _, ok := obj.(*microflows.ExclusiveSplit); ok {
189+
split = obj
190+
break
191+
}
192+
}
193+
if split == nil {
194+
t.Fatal("expected exclusive split")
195+
}
196+
197+
flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{}
198+
flowsByDest := map[model.ID][]*microflows.SequenceFlow{}
199+
for _, flow := range oc.Flows {
200+
flowsByOrigin[flow.OriginID] = append(flowsByOrigin[flow.OriginID], flow)
201+
flowsByDest[flow.DestinationID] = append(flowsByDest[flow.DestinationID], flow)
202+
}
203+
204+
var lines []string
205+
emitAnchorAnnotation(split, flowsByOrigin, flowsByDest, &lines, "")
206+
if len(lines) != 0 {
207+
t.Fatalf("expected false branch from-top default anchor to be omitted, got %q", strings.Join(lines, "\n"))
208+
}
209+
}
210+
166211
func TestBuilder_AnchorToTopOnReturnPreservedInsideElse(t *testing.T) {
167212
// Minimal case: single-statement ELSE whose only statement is a RETURN
168213
// carrying @anchor(to: top). The flow from the split to that return's

mdl/executor/cmd_microflows_describe_anchor_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,38 @@ func TestEmitAnchorAnnotation_FromAndTo(t *testing.T) {
5050
}
5151
}
5252

53+
func TestEmitAnchorAnnotation_OmitsDefaultRightToLeft(t *testing.T) {
54+
activity := &microflows.ActionActivity{
55+
BaseActivity: microflows.BaseActivity{
56+
BaseMicroflowObject: microflows.BaseMicroflowObject{
57+
BaseElement: model.BaseElement{ID: "act-default"},
58+
},
59+
},
60+
}
61+
incoming := &microflows.SequenceFlow{
62+
DestinationID: "act-default",
63+
DestinationConnectionIndex: AnchorLeft,
64+
}
65+
outgoing := &microflows.SequenceFlow{
66+
OriginID: "act-default",
67+
OriginConnectionIndex: AnchorRight,
68+
}
69+
70+
flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{
71+
"act-default": {outgoing},
72+
}
73+
flowsByDest := map[model.ID][]*microflows.SequenceFlow{
74+
"act-default": {incoming},
75+
}
76+
77+
var lines []string
78+
emitAnchorAnnotation(activity, flowsByOrigin, flowsByDest, &lines, "")
79+
80+
if len(lines) != 0 {
81+
t.Fatalf("expected default anchor line to be omitted, got %v", lines)
82+
}
83+
}
84+
5385
func TestEmitAnchorAnnotation_NoFlowsSkipsEmission(t *testing.T) {
5486
activity := &microflows.ActionActivity{
5587
BaseActivity: microflows.BaseActivity{
@@ -66,6 +98,53 @@ func TestEmitAnchorAnnotation_NoFlowsSkipsEmission(t *testing.T) {
6698
}
6799
}
68100

101+
func TestEmitAnchorAnnotation_IgnoresLoopBodyTailOutgoingFlow(t *testing.T) {
102+
activity := &microflows.ActionActivity{
103+
BaseActivity: microflows.BaseActivity{
104+
BaseMicroflowObject: microflows.BaseMicroflowObject{
105+
BaseElement: model.BaseElement{ID: "body-act"},
106+
},
107+
},
108+
}
109+
loop := &microflows.LoopedActivity{
110+
BaseMicroflowObject: microflows.BaseMicroflowObject{
111+
BaseElement: model.BaseElement{ID: "loop-1"},
112+
},
113+
ObjectCollection: &microflows.MicroflowObjectCollection{
114+
Objects: []microflows.MicroflowObject{activity},
115+
},
116+
}
117+
118+
flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{
119+
"body-act": {
120+
{
121+
OriginID: "body-act",
122+
DestinationID: "loop-1",
123+
OriginConnectionIndex: AnchorLeft,
124+
},
125+
},
126+
}
127+
flowsByDest := map[model.ID][]*microflows.SequenceFlow{
128+
"body-act": {
129+
{
130+
DestinationID: "body-act",
131+
DestinationConnectionIndex: AnchorLeft,
132+
},
133+
},
134+
}
135+
activityMap := map[model.ID]microflows.MicroflowObject{
136+
"body-act": activity,
137+
"loop-1": loop,
138+
}
139+
140+
var lines []string
141+
emitAnchorAnnotationWithActivityMap(activity, flowsByOrigin, flowsByDest, activityMap, &lines, "")
142+
143+
if len(lines) != 0 {
144+
t.Fatalf("expected loop body tail flow to be ignored, got %v", lines)
145+
}
146+
}
147+
69148
func TestAnchorRoundtripViaParserBuilder(t *testing.T) {
70149
// Build an AST with an @anchor on a statement, run it through the builder,
71150
// and verify the resulting SequenceFlow has the right anchors. Then the

mdl/executor/cmd_microflows_describe_concurrent_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestFormatMicroflowActivities_Concurrent_NoRace(t *testing.T) {
2626
// Build two distinct microflows whose activities are anchored to
2727
// different sides. If the two describe calls share state, one will
2828
// emit the other's anchor keyword.
29-
mfA := mkRaceMicroflow("mfa-start", "mfa-log", "mfa-end", AnchorRight)
29+
mfA := mkRaceMicroflow("mfa-start", "mfa-log", "mfa-end", AnchorTop)
3030
mfB := mkRaceMicroflow("mfb-start", "mfb-log", "mfb-end", AnchorBottom)
3131

3232
e := newTestExecutor()
@@ -50,8 +50,8 @@ func TestFormatMicroflowActivities_Concurrent_NoRace(t *testing.T) {
5050
}
5151
wg.Wait()
5252

53-
wantA := "@anchor(from: right, to: left)"
54-
wantB := "@anchor(from: bottom, to: left)"
53+
wantA := "@anchor(from: top)"
54+
wantB := "@anchor(from: bottom)"
5555
for i, got := range resultsA {
5656
if !strings.Contains(got, wantA) {
5757
t.Errorf("worker %d (A) missing %q in output:\n%s", i, wantA, got)

0 commit comments

Comments
 (0)