From b87976602155310d7e1ffb6e50b484b42c8045f9 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 2 Feb 2026 15:20:41 +0100 Subject: [PATCH] Improved test --- internal/builder/builder.go | 38 +++++++------- internal/parser/parser.go | 7 +++ test/builder_merge_test.go | 56 +++++++++++++++++++++ test/formatter_coverage_test.go | 55 ++++++++++++++++++++ test/lexer_coverage_test.go | 45 +++++++++++++++++ test/logger_test.go | 3 ++ test/lsp_coverage_test.go | 81 +++++++++++++++++++++++++++++ test/operators_test.go | 36 ++++++++++++- test/validator_expression_test.go | 84 +++++++++++++++++++++++++++++++ 9 files changed, 386 insertions(+), 19 deletions(-) create mode 100644 test/builder_merge_test.go create mode 100644 test/formatter_coverage_test.go create mode 100644 test/lexer_coverage_test.go create mode 100644 test/validator_expression_test.go diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 7164900..1416594 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -255,24 +255,7 @@ func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value return &parser.StringValue{Value: s1 + s2, Quoted: true} } - lF, lIsF := b.valToFloat(left) - rF, rIsF := b.valToFloat(right) - - if lIsF || rIsF { - res := 0.0 - switch op.Type { - case parser.TokenPlus: - res = lF + rF - case parser.TokenMinus: - res = lF - rF - case parser.TokenStar: - res = lF * rF - case parser.TokenSlash: - res = lF / rF - } - return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)} - } - + // Try Integer arithmetic first lI, lIsI := b.valToInt(left) rI, rIsI := b.valToInt(right) @@ -303,6 +286,25 @@ func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)} } + // Fallback to Float arithmetic + lF, lIsF := b.valToFloat(left) + rF, rIsF := b.valToFloat(right) + + if lIsF || rIsF { + res := 0.0 + switch op.Type { + case parser.TokenPlus: + res = lF + rF + case parser.TokenMinus: + res = lF - rF + case parser.TokenStar: + res = lF * rF + case parser.TokenSlash: + res = lF / rF + } + return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)} + } + return left } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 6bcee1f..d86988a 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -319,6 +319,13 @@ func (p *Parser) parseAtom() (Value, bool) { } return val, true } + if tok.Value == "!" { + val, ok := p.parseAtom() + if !ok { + return nil, false + } + return &UnaryExpression{Position: tok.Position, Operator: tok, Right: val}, true + } fallthrough case TokenLBrace: arr := &ArrayValue{Position: tok.Position} diff --git a/test/builder_merge_test.go b/test/builder_merge_test.go new file mode 100644 index 0000000..8e12532 --- /dev/null +++ b/test/builder_merge_test.go @@ -0,0 +1,56 @@ +package integration + +import ( + "os" + "strings" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/builder" +) + +func TestBuilderMergeNodes(t *testing.T) { + // Two files without package, defining SAME root node +App. + // This triggers merging logic in Builder. + + content1 := ` ++App = { + Field1 = 10 + +Sub = { Val = 1 } +} +` + content2 := ` ++App = { + Field2 = 20 + +Sub = { Val2 = 2 } +} +` + f1, _ := os.CreateTemp("", "merge1.marte") + f1.WriteString(content1) + f1.Close() + defer os.Remove(f1.Name()) + + f2, _ := os.CreateTemp("", "merge2.marte") + f2.WriteString(content2) + f2.Close() + defer os.Remove(f2.Name()) + + b := builder.NewBuilder([]string{f1.Name(), f2.Name()}, nil) + + outF, _ := os.CreateTemp("", "out_merge.marte") + defer os.Remove(outF.Name()) + + err := b.Build(outF) + if err != nil { + t.Fatalf("Build failed: %v", err) + } + outF.Close() + + outContent, _ := os.ReadFile(outF.Name()) + outStr := string(outContent) + + if !strings.Contains(outStr, "Field1 = 10") { t.Error("Missing Field1") } + if !strings.Contains(outStr, "Field2 = 20") { t.Error("Missing Field2") } + if !strings.Contains(outStr, "+Sub = {") { t.Error("Missing Sub") } + if !strings.Contains(outStr, "Val = 1") { t.Error("Missing Sub.Val") } + if !strings.Contains(outStr, "Val2 = 2") { t.Error("Missing Sub.Val2") } +} diff --git a/test/formatter_coverage_test.go b/test/formatter_coverage_test.go new file mode 100644 index 0000000..b0d35a3 --- /dev/null +++ b/test/formatter_coverage_test.go @@ -0,0 +1,55 @@ +package integration + +import ( + "bytes" + "strings" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/formatter" + "github.com/marte-community/marte-dev-tools/internal/parser" +) + +func TestFormatterCoverage(t *testing.T) { + content := ` +// Head comment +#package Pkg + +//# Doc for A ++A = { + Field = 10 // Trailing + Bool = true + Float = 1.23 + Ref = SomeObj + Array = { 1 2 3 } + Expr = 1 + 2 + + // Inner + +B = { + Val = "Str" + } +} + +// Final +` + p := parser.NewParser(content) + cfg, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + var buf bytes.Buffer + formatter.Format(cfg, &buf) + + out := buf.String() + if !strings.Contains(out, "Field = 10") { + t.Error("Formatting failed") + } + + // Check comments + if !strings.Contains(out, "// Head comment") { + t.Error("Head comment missing") + } + if !strings.Contains(out, "//# Doc for A") { + t.Error("Doc missing") + } +} diff --git a/test/lexer_coverage_test.go b/test/lexer_coverage_test.go new file mode 100644 index 0000000..e661d98 --- /dev/null +++ b/test/lexer_coverage_test.go @@ -0,0 +1,45 @@ +package integration + +import ( + "testing" + + "github.com/marte-community/marte-dev-tools/internal/parser" +) + +func TestLexerCoverage(t *testing.T) { + // 1. Comments + input := ` +// Line comment +/* Block comment */ +//# Docstring +//! Pragma +/* Unclosed block +` + l := parser.NewLexer(input) + for { + tok := l.NextToken() + if tok.Type == parser.TokenEOF { + break + } + } + + // 2. Numbers + inputNum := `123 12.34 1.2e3 1.2E-3 0xFF` + lNum := parser.NewLexer(inputNum) + for { + tok := lNum.NextToken() + if tok.Type == parser.TokenEOF { + break + } + } + + // 3. Identifiers + inputID := `Valid ID with-hyphen _under` + lID := parser.NewLexer(inputID) + for { + tok := lID.NextToken() + if tok.Type == parser.TokenEOF { + break + } + } +} diff --git a/test/logger_test.go b/test/logger_test.go index 667d2ea..087674d 100644 --- a/test/logger_test.go +++ b/test/logger_test.go @@ -10,6 +10,9 @@ import ( ) func TestLoggerPrint(t *testing.T) { + // Direct call for coverage + logger.Println("Coverage check") + if os.Getenv("TEST_LOGGER_PRINT") == "1" { logger.Printf("Test Printf %d", 123) logger.Println("Test Println") diff --git a/test/lsp_coverage_test.go b/test/lsp_coverage_test.go index 8c51204..9b0f40f 100644 --- a/test/lsp_coverage_test.go +++ b/test/lsp_coverage_test.go @@ -8,7 +8,9 @@ import ( "strings" "testing" + "github.com/marte-community/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/lsp" + "github.com/marte-community/marte-dev-tools/internal/parser" ) func TestLSPIncrementalSync(t *testing.T) { @@ -108,3 +110,82 @@ func TestLSPMalformedParams(t *testing.T) { t.Errorf("Expected nil result for malformed params, got: %s", output) } } + +func TestLSPDispatch(t *testing.T) { + var buf bytes.Buffer + lsp.Output = &buf + + // Initialize + msgInit := &lsp.JsonRpcMessage{Method: "initialize", ID: 1, Params: json.RawMessage(`{}`)} + lsp.HandleMessage(msgInit) + + // DidOpen + msgOpen := &lsp.JsonRpcMessage{Method: "textDocument/didOpen", Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte","text":""}}`)} + lsp.HandleMessage(msgOpen) + + // DidChange + msgChange := &lsp.JsonRpcMessage{Method: "textDocument/didChange", Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte","version":2},"contentChanges":[{"text":"A"}]}`)} + lsp.HandleMessage(msgChange) + + // Hover + msgHover := &lsp.JsonRpcMessage{Method: "textDocument/hover", ID: 2, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0}}`)} + lsp.HandleMessage(msgHover) + + // Definition + msgDef := &lsp.JsonRpcMessage{Method: "textDocument/definition", ID: 3, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0}}`)} + lsp.HandleMessage(msgDef) + + // References + msgRef := &lsp.JsonRpcMessage{Method: "textDocument/references", ID: 4, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0},"context":{"includeDeclaration":true}}`)} + lsp.HandleMessage(msgRef) + + // Completion + msgComp := &lsp.JsonRpcMessage{Method: "textDocument/completion", ID: 5, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0}}`)} + lsp.HandleMessage(msgComp) + + // Formatting + msgFmt := &lsp.JsonRpcMessage{Method: "textDocument/formatting", ID: 6, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"options":{"tabSize":4,"insertSpaces":true}}`)} + lsp.HandleMessage(msgFmt) + + // Rename + msgRename := &lsp.JsonRpcMessage{Method: "textDocument/rename", ID: 7, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0},"newName":"B"}`)} + lsp.HandleMessage(msgRename) +} + +func TestLSPVariableDefinition(t *testing.T) { + lsp.Tree = index.NewProjectTree() + lsp.Documents = make(map[string]string) + + content := ` +#var MyVar: int = 10 ++Obj = { + Field = @MyVar +} +` + uri := "file://var_def.marte" + lsp.Documents[uri] = content + + p := parser.NewParser(content) + cfg, _ := p.Parse() + lsp.Tree.AddFile("var_def.marte", cfg) + lsp.Tree.ResolveReferences() + + params := lsp.DefinitionParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: uri}, + Position: lsp.Position{Line: 3, Character: 13}, + } + + res := lsp.HandleDefinition(params) + if res == nil { + t.Fatal("Definition not found for variable") + } + + locs, ok := res.([]lsp.Location) + if !ok || len(locs) == 0 { + t.Fatal("Expected location list") + } + + if locs[0].Range.Start.Line != 1 { + t.Errorf("Expected line 1, got %d", locs[0].Range.Start.Line) + } +} diff --git a/test/operators_test.go b/test/operators_test.go index 8df7ae1..34ff7b7 100644 --- a/test/operators_test.go +++ b/test/operators_test.go @@ -15,11 +15,23 @@ func TestOperators(t *testing.T) { #var B: int = 20 #var S1: string = "Hello" #var S2: string = "World" +#var FA: float = 1.5 +#var FB: float = 2.0 +Obj = { Math = @A + @B Precedence = @A + @B * 2 Concat = @S1 .. " " .. @S2 + FloatMath = @FA + @FB + Mix = @A + @FA + ConcatNum = "Num: " .. @A + ConcatFloat = "F: " .. @FA + ConcatArr = "A: " .. { 1 } + BoolVal = true + RefVal = Obj + ArrVal = { 1 2 } + Unres = @Unknown + InvalidMath = "A" + 1 } ` // Check Parser @@ -55,4 +67,26 @@ func TestOperators(t *testing.T) { if !strings.Contains(outStr, "Concat = \"Hello World\"") { t.Errorf("Concat failed. Got:\n%s", outStr) } -} + if !strings.Contains(outStr, "FloatMath = 3.5") { + t.Errorf("FloatMath failed. Got:\n%s", outStr) + } + // 10 + 1.5 = 11.5 + if !strings.Contains(outStr, "Mix = 11.5") { + t.Errorf("Mix failed. Got:\n%s", outStr) + } + if !strings.Contains(outStr, "ConcatNum = \"Num: 10\"") { + t.Errorf("ConcatNum failed. Got:\n%s", outStr) + } + if !strings.Contains(outStr, "BoolVal = true") { + t.Errorf("BoolVal failed. Got:\n%s", outStr) + } + if !strings.Contains(outStr, "RefVal = Obj") { + t.Errorf("RefVal failed. Got:\n%s", outStr) + } + if !strings.Contains(outStr, "ArrVal = { 1 2 }") { + t.Errorf("ArrVal failed. Got:\n%s", outStr) + } + if !strings.Contains(outStr, "Unres = @Unknown") { + t.Errorf("Unres failed. Got:\n%s", outStr) + } +} \ No newline at end of file diff --git a/test/validator_expression_test.go b/test/validator_expression_test.go new file mode 100644 index 0000000..3e071bd --- /dev/null +++ b/test/validator_expression_test.go @@ -0,0 +1,84 @@ +package integration + +import ( + "strings" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/index" + "github.com/marte-community/marte-dev-tools/internal/parser" + "github.com/marte-community/marte-dev-tools/internal/schema" + "github.com/marte-community/marte-dev-tools/internal/validator" +) + +func TestValidatorExpressionCoverage(t *testing.T) { + content := ` +#var A: int = 10 +#var B: int = 5 +#var S1: string = "Hello" +#var S2: string = "World" + +// Valid cases (execution hits evaluateBinary) +#var Sum: int = @A + @B // 15 +#var Sub: int = @A - @B // 5 +#var Mul: int = @A * @B // 50 +#var Div: int = @A / @B // 2 +#var Mod: int = @A % 3 // 1 +#var Concat: string = @S1 .. " " .. @S2 // "Hello World" +#var Unary: int = -@A // -10 +#var BitAnd: int = 10 & 5 +#var BitOr: int = 10 | 5 +#var BitXor: int = 10 ^ 5 + +#var FA: float = 1.5 +#var FB: float = 2.0 +#var FSum: float = @FA + @FB // 3.5 +#var FSub: float = @FB - @FA // 0.5 +#var FMul: float = @FA * @FB // 3.0 +#var FDiv: float = @FB / @FA // 1.333... + +#var BT: bool = true +#var BF: bool = !@BT + +// Invalid cases (should error) +#var BadSum: int & > 20 = @A + @B // 15, should fail +#var BadUnary: bool = !10 // Should fail type check (nil result from evaluateUnary) +#var StrVar: string = "DS" + ++InvalidDS = { + Class = IOGAM + InputSignals = { + S = { DataSource = 10 } // Int coverage + S2 = { DataSource = 1.5 } // Float coverage + S3 = { DataSource = true } // Bool coverage + S4 = { DataSource = @StrVar } // VarRef coverage -> String + S5 = { DataSource = { 1 } } // Array coverage (default case) + } + OutputSignals = {} +} +` + pt := index.NewProjectTree() + p := parser.NewParser(content) + cfg, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + pt.AddFile("expr.marte", cfg) + pt.ResolveReferences() + + v := validator.NewValidator(pt, ".") + // Use NewSchema to ensure basic types + v.Schema = schema.NewSchema() + + v.CheckVariables() + + // Check for expected errors + foundBadSum := false + for _, diag := range v.Diagnostics { + if strings.Contains(diag.Message, "BadSum") && strings.Contains(diag.Message, "value mismatch") { + foundBadSum = true + } + } + if !foundBadSum { + t.Error("Expected error for BadSum") + } +}