Skip to content

Commit 53b7866

Browse files
committed
test: add regression tests for issues #18, #19, #23, #25, #26, #27, #28
26 unit tests covering: - #18: loopEndKeyword (WHILE vs LOOP), WHILE loop header formatting - #19: Long type mapping (convertASTToMicroflowDataType, GetTypeName) - #25: DESCRIBE CONSTANT COMMENT output, escaping, absence - #26: Date vs DateTime type mapping and constant formatting - #27: isQualifiedEnumLiteral, enum RETURN without $ prefix - #28: inline if-then-else parsing and expressionToString serialization - #23: deriveColumnName from attribute, caption, fallback, special chars Issue #20 (XPath tokens) already covered by bugfix_test.go.
1 parent 4ac63e5 commit 53b7866

1 file changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Regression tests for GitHub Issues #18, #19, #23, #25, #26, #27, #28.
4+
package executor
5+
6+
import (
7+
"bytes"
8+
"strings"
9+
"testing"
10+
11+
"github.com/mendixlabs/mxcli/mdl/ast"
12+
"github.com/mendixlabs/mxcli/mdl/visitor"
13+
"github.com/mendixlabs/mxcli/model"
14+
"github.com/mendixlabs/mxcli/sdk/microflows"
15+
)
16+
17+
// =============================================================================
18+
// Issue #18: DESCRIBE MICROFLOW emits END WHILE and traverses WHILE loop body
19+
// =============================================================================
20+
21+
// TestLoopEndKeyword_WhileLoop verifies loopEndKeyword returns "END WHILE" for WHILE loops.
22+
func TestLoopEndKeyword_WhileLoop(t *testing.T) {
23+
loop := &microflows.LoopedActivity{
24+
LoopSource: &microflows.WhileLoopCondition{WhileExpression: "$Counter < 10"},
25+
}
26+
got := loopEndKeyword(loop)
27+
if got != "END WHILE" {
28+
t.Errorf("loopEndKeyword(WhileLoop) = %q, want %q", got, "END WHILE")
29+
}
30+
}
31+
32+
// TestLoopEndKeyword_ForEachLoop verifies loopEndKeyword returns "END LOOP" for FOR-EACH loops.
33+
func TestLoopEndKeyword_ForEachLoop(t *testing.T) {
34+
loop := &microflows.LoopedActivity{
35+
LoopSource: &microflows.IterableList{
36+
VariableName: "Item",
37+
ListVariableName: "Items",
38+
},
39+
}
40+
got := loopEndKeyword(loop)
41+
if got != "END LOOP" {
42+
t.Errorf("loopEndKeyword(ForEachLoop) = %q, want %q", got, "END LOOP")
43+
}
44+
}
45+
46+
// TestLoopEndKeyword_NilSource verifies loopEndKeyword returns "END LOOP" when LoopSource is nil.
47+
func TestLoopEndKeyword_NilSource(t *testing.T) {
48+
loop := &microflows.LoopedActivity{}
49+
got := loopEndKeyword(loop)
50+
if got != "END LOOP" {
51+
t.Errorf("loopEndKeyword(nil) = %q, want %q", got, "END LOOP")
52+
}
53+
}
54+
55+
// TestFormatActivity_WhileLoop verifies WHILE loop header formatting.
56+
func TestFormatActivity_WhileLoop(t *testing.T) {
57+
e := newTestExecutor()
58+
obj := &microflows.LoopedActivity{
59+
BaseMicroflowObject: mkObj("1"),
60+
LoopSource: &microflows.WhileLoopCondition{WhileExpression: "$Counter <= $N"},
61+
}
62+
got := e.formatActivity(obj, nil, nil)
63+
if got != "WHILE $Counter <= $N" {
64+
t.Errorf("got %q, want %q", got, "WHILE $Counter <= $N")
65+
}
66+
}
67+
68+
// =============================================================================
69+
// Issue #19: Long type must not be downgraded to Integer
70+
// =============================================================================
71+
72+
// TestConvertASTToMicroflowDataType_Long verifies Long maps to LongType, not IntegerType.
73+
func TestConvertASTToMicroflowDataType_Long(t *testing.T) {
74+
dt := ast.DataType{Kind: ast.TypeLong}
75+
result := convertASTToMicroflowDataType(dt, nil)
76+
if _, ok := result.(*microflows.LongType); !ok {
77+
t.Errorf("expected *microflows.LongType, got %T", result)
78+
}
79+
}
80+
81+
// TestConvertASTToMicroflowDataType_Integer verifies Integer maps to IntegerType (not affected by Long fix).
82+
func TestConvertASTToMicroflowDataType_Integer(t *testing.T) {
83+
dt := ast.DataType{Kind: ast.TypeInteger}
84+
result := convertASTToMicroflowDataType(dt, nil)
85+
if _, ok := result.(*microflows.IntegerType); !ok {
86+
t.Errorf("expected *microflows.IntegerType, got %T", result)
87+
}
88+
}
89+
90+
// TestLongType_GetTypeName verifies LongType.GetTypeName() returns "Long".
91+
func TestLongType_GetTypeName(t *testing.T) {
92+
lt := &microflows.LongType{}
93+
if got := lt.GetTypeName(); got != "Long" {
94+
t.Errorf("LongType.GetTypeName() = %q, want %q", got, "Long")
95+
}
96+
}
97+
98+
// =============================================================================
99+
// Issue #25: DESCRIBE CONSTANT emits COMMENT field
100+
// =============================================================================
101+
102+
// TestOutputConstantMDL_WithComment verifies DESCRIBE CONSTANT includes COMMENT clause.
103+
func TestOutputConstantMDL_WithComment(t *testing.T) {
104+
buf := &bytes.Buffer{}
105+
e := New(buf)
106+
c := &model.Constant{
107+
Name: "MaxRetries",
108+
Type: model.ConstantDataType{Kind: "Integer"},
109+
DefaultValue: "3",
110+
Documentation: "Maximum retry attempts",
111+
}
112+
if err := e.outputConstantMDL(c, "MyModule"); err != nil {
113+
t.Fatalf("outputConstantMDL: %v", err)
114+
}
115+
gotStr := buf.String()
116+
if !strings.Contains(gotStr, "COMMENT 'Maximum retry attempts'") {
117+
t.Errorf("expected COMMENT clause in output, got:\n%s", gotStr)
118+
}
119+
}
120+
121+
// TestOutputConstantMDL_WithoutComment verifies DESCRIBE CONSTANT without COMMENT omits it.
122+
func TestOutputConstantMDL_WithoutComment(t *testing.T) {
123+
buf := &bytes.Buffer{}
124+
e := New(buf)
125+
c := &model.Constant{
126+
Name: "AppName",
127+
Type: model.ConstantDataType{Kind: "String"},
128+
DefaultValue: "'MyApp'",
129+
}
130+
if err := e.outputConstantMDL(c, "MyModule"); err != nil {
131+
t.Fatalf("outputConstantMDL: %v", err)
132+
}
133+
gotStr := buf.String()
134+
if strings.Contains(gotStr, "COMMENT") {
135+
t.Errorf("expected no COMMENT clause, got:\n%s", gotStr)
136+
}
137+
}
138+
139+
// TestOutputConstantMDL_CommentEscapesSingleQuotes verifies quotes in COMMENT are escaped.
140+
func TestOutputConstantMDL_CommentEscapesSingleQuotes(t *testing.T) {
141+
buf := &bytes.Buffer{}
142+
e := New(buf)
143+
c := &model.Constant{
144+
Name: "Greeting",
145+
Type: model.ConstantDataType{Kind: "String"},
146+
DefaultValue: "'hello'",
147+
Documentation: "It's a test",
148+
}
149+
if err := e.outputConstantMDL(c, "MyModule"); err != nil {
150+
t.Fatalf("outputConstantMDL: %v", err)
151+
}
152+
gotStr := buf.String()
153+
if !strings.Contains(gotStr, "COMMENT 'It''s a test'") {
154+
t.Errorf("expected escaped quote in COMMENT, got:\n%s", gotStr)
155+
}
156+
}
157+
158+
// =============================================================================
159+
// Issue #26: Date type distinct from DateTime
160+
// =============================================================================
161+
162+
// TestConvertASTToMicroflowDataType_Date verifies Date maps to DateType (not DateTimeType).
163+
func TestConvertASTToMicroflowDataType_Date(t *testing.T) {
164+
dt := ast.DataType{Kind: ast.TypeDate}
165+
result := convertASTToMicroflowDataType(dt, nil)
166+
if _, ok := result.(*microflows.DateType); !ok {
167+
t.Errorf("expected *microflows.DateType, got %T", result)
168+
}
169+
}
170+
171+
// TestConvertASTToMicroflowDataType_DateTime verifies DateTime maps to DateTimeType (not affected by Date fix).
172+
func TestConvertASTToMicroflowDataType_DateTime(t *testing.T) {
173+
dt := ast.DataType{Kind: ast.TypeDateTime}
174+
result := convertASTToMicroflowDataType(dt, nil)
175+
if _, ok := result.(*microflows.DateTimeType); !ok {
176+
t.Errorf("expected *microflows.DateTimeType, got %T", result)
177+
}
178+
}
179+
180+
// TestDateType_GetTypeName verifies DateType.GetTypeName() returns "Date".
181+
func TestDateType_GetTypeName(t *testing.T) {
182+
dt := &microflows.DateType{}
183+
if got := dt.GetTypeName(); got != "Date" {
184+
t.Errorf("DateType.GetTypeName() = %q, want %q", got, "Date")
185+
}
186+
}
187+
188+
// TestFormatConstantType_Date verifies Date constant type formatting.
189+
func TestFormatConstantType_Date(t *testing.T) {
190+
got := formatConstantType(model.ConstantDataType{Kind: "Date"})
191+
if got != "Date" {
192+
t.Errorf("formatConstantType(Date) = %q, want %q", got, "Date")
193+
}
194+
}
195+
196+
// TestFormatConstantType_DateTime verifies DateTime constant type formatting.
197+
func TestFormatConstantType_DateTime(t *testing.T) {
198+
got := formatConstantType(model.ConstantDataType{Kind: "DateTime"})
199+
if got != "DateTime" {
200+
t.Errorf("formatConstantType(DateTime) = %q, want %q", got, "DateTime")
201+
}
202+
}
203+
204+
// =============================================================================
205+
// Issue #27: DESCRIBE omits incorrect $ prefix on enum literal in RETURN
206+
// =============================================================================
207+
208+
// TestIsQualifiedEnumLiteral verifies enum literal detection.
209+
func TestIsQualifiedEnumLiteral(t *testing.T) {
210+
tests := []struct {
211+
input string
212+
want bool
213+
}{
214+
{"Module.Status.Active", true},
215+
{"System.ENUM_Type.Value", true},
216+
{"MyVar", false},
217+
{"", false},
218+
{"empty", false},
219+
{"true", false},
220+
}
221+
for _, tt := range tests {
222+
t.Run(tt.input, func(t *testing.T) {
223+
got := isQualifiedEnumLiteral(tt.input)
224+
if got != tt.want {
225+
t.Errorf("isQualifiedEnumLiteral(%q) = %v, want %v", tt.input, got, tt.want)
226+
}
227+
})
228+
}
229+
}
230+
231+
// TestFormatActivity_EndEvent_EnumReturn verifies enum RETURN has no $ prefix.
232+
func TestFormatActivity_EndEvent_EnumReturn(t *testing.T) {
233+
e := newTestExecutor()
234+
obj := &microflows.EndEvent{
235+
BaseMicroflowObject: mkObj("1"),
236+
ReturnValue: "Module.Status.Active",
237+
}
238+
got := e.formatActivity(obj, nil, nil)
239+
want := "RETURN Module.Status.Active;"
240+
if got != want {
241+
t.Errorf("got %q, want %q", got, want)
242+
}
243+
}
244+
245+
// TestFormatActivity_EndEvent_VariableReturn verifies variable RETURN keeps $ prefix.
246+
func TestFormatActivity_EndEvent_VariableReturn(t *testing.T) {
247+
e := newTestExecutor()
248+
obj := &microflows.EndEvent{
249+
BaseMicroflowObject: mkObj("1"),
250+
ReturnValue: "Result",
251+
}
252+
got := e.formatActivity(obj, nil, nil)
253+
want := "RETURN $Result;"
254+
if got != want {
255+
t.Errorf("got %q, want %q", got, want)
256+
}
257+
}
258+
259+
// =============================================================================
260+
// Issue #28: Inline if-then-else expression parsing and serialization
261+
// =============================================================================
262+
263+
// TestParseInlineIfThenElse verifies the parser handles inline if-then-else in SET.
264+
func TestParseInlineIfThenElse(t *testing.T) {
265+
input := `CREATE MICROFLOW Test.InlineIf ()
266+
BEGIN
267+
DECLARE $X Integer = 0;
268+
SET $X = if $X > 10 then 1 else 0;
269+
END;`
270+
271+
prog, errs := visitor.Build(input)
272+
if len(errs) > 0 {
273+
t.Fatalf("Parse error: %v", errs[0])
274+
}
275+
if len(prog.Statements) == 0 {
276+
t.Fatal("No statements parsed")
277+
}
278+
279+
stmt, ok := prog.Statements[0].(*ast.CreateMicroflowStmt)
280+
if !ok {
281+
t.Fatalf("Expected CreateMicroflowStmt, got %T", prog.Statements[0])
282+
}
283+
284+
// Find the SET statement which should contain an IfThenElseExpr
285+
found := false
286+
for _, action := range stmt.Body {
287+
if setStmt, ok := action.(*ast.MfSetStmt); ok {
288+
if _, isIf := setStmt.Value.(*ast.IfThenElseExpr); isIf {
289+
found = true
290+
}
291+
}
292+
}
293+
if !found {
294+
t.Error("Expected SET with IfThenElseExpr, not found in parsed body")
295+
}
296+
}
297+
298+
// TestExpressionToString_IfThenElse verifies inline if-then-else serialization.
299+
func TestExpressionToString_IfThenElse(t *testing.T) {
300+
expr := &ast.IfThenElseExpr{
301+
Condition: &ast.BinaryExpr{
302+
Left: &ast.VariableExpr{Name: "X"},
303+
Operator: ">",
304+
Right: &ast.LiteralExpr{Value: int64(10), Kind: ast.LiteralInteger},
305+
},
306+
ThenExpr: &ast.VariableExpr{Name: "A"},
307+
ElseExpr: &ast.VariableExpr{Name: "B"},
308+
}
309+
got := expressionToString(expr)
310+
want := "if $X > 10 then $A else $B"
311+
if got != want {
312+
t.Errorf("expressionToString(IfThenElse) = %q, want %q", got, want)
313+
}
314+
}
315+
316+
// TestExpressionToString_NestedIfThenElse verifies nested inline if-then-else.
317+
func TestExpressionToString_NestedIfThenElse(t *testing.T) {
318+
expr := &ast.IfThenElseExpr{
319+
Condition: &ast.BinaryExpr{
320+
Left: &ast.VariableExpr{Name: "X"},
321+
Operator: ">",
322+
Right: &ast.LiteralExpr{Value: int64(0), Kind: ast.LiteralInteger},
323+
},
324+
ThenExpr: &ast.IfThenElseExpr{
325+
Condition: &ast.BinaryExpr{
326+
Left: &ast.VariableExpr{Name: "X"},
327+
Operator: ">",
328+
Right: &ast.LiteralExpr{Value: int64(100), Kind: ast.LiteralInteger},
329+
},
330+
ThenExpr: &ast.LiteralExpr{Value: int64(1), Kind: ast.LiteralInteger},
331+
ElseExpr: &ast.LiteralExpr{Value: int64(2), Kind: ast.LiteralInteger},
332+
},
333+
ElseExpr: &ast.LiteralExpr{Value: int64(3), Kind: ast.LiteralInteger},
334+
}
335+
got := expressionToString(expr)
336+
want := "if $X > 0 then if $X > 100 then 1 else 2 else 3"
337+
if got != want {
338+
t.Errorf("expressionToString(nested IfThenElse) = %q, want %q", got, want)
339+
}
340+
}
341+
342+
// =============================================================================
343+
// Issue #23: DataGrid2 column names derived from attribute or caption
344+
// =============================================================================
345+
346+
// TestDeriveColumnName_FromAttribute verifies column name from attribute.
347+
func TestDeriveColumnName_FromAttribute(t *testing.T) {
348+
col := rawDataGridColumn{Attribute: "MyModule.Order.OrderDate"}
349+
got := deriveColumnName(col, 0)
350+
if got != "OrderDate" {
351+
t.Errorf("deriveColumnName(attribute) = %q, want %q", got, "OrderDate")
352+
}
353+
}
354+
355+
// TestDeriveColumnName_FromCaption verifies column name from caption.
356+
func TestDeriveColumnName_FromCaption(t *testing.T) {
357+
col := rawDataGridColumn{Caption: "Order Date"}
358+
got := deriveColumnName(col, 0)
359+
if got != "Order_Date" {
360+
t.Errorf("deriveColumnName(caption) = %q, want %q", got, "Order_Date")
361+
}
362+
}
363+
364+
// TestDeriveColumnName_Fallback verifies fallback to col%d.
365+
func TestDeriveColumnName_Fallback(t *testing.T) {
366+
col := rawDataGridColumn{}
367+
got := deriveColumnName(col, 2)
368+
if got != "col3" {
369+
t.Errorf("deriveColumnName(empty) = %q, want %q", got, "col3")
370+
}
371+
}
372+
373+
// TestDeriveColumnName_AttributePrecedence verifies attribute takes precedence over caption.
374+
func TestDeriveColumnName_AttributePrecedence(t *testing.T) {
375+
col := rawDataGridColumn{
376+
Attribute: "MyModule.Order.Status",
377+
Caption: "Order Status",
378+
}
379+
got := deriveColumnName(col, 0)
380+
if got != "Status" {
381+
t.Errorf("deriveColumnName(both) = %q, want %q", got, "Status")
382+
}
383+
}
384+
385+
// TestDeriveColumnName_CaptionSpecialChars verifies caption sanitization.
386+
func TestDeriveColumnName_CaptionSpecialChars(t *testing.T) {
387+
col := rawDataGridColumn{Caption: "Order #ID (main)"}
388+
got := deriveColumnName(col, 0)
389+
if got != "Order__ID__main" {
390+
t.Errorf("deriveColumnName(special chars) = %q, want %q", got, "Order__ID__main")
391+
}
392+
}

0 commit comments

Comments
 (0)