Improved lsp + builder + using logger

This commit is contained in:
Martino Ferrari
2026-01-21 14:35:30 +01:00
parent d4d857bf05
commit f3c13fca55
21 changed files with 891 additions and 170 deletions

View File

@@ -0,0 +1,97 @@
package integration
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/builder"
)
func TestMultiFileBuildMergeAndOrder(t *testing.T) {
// Setup
os.RemoveAll("build_multi_test")
os.MkdirAll("build_multi_test", 0755)
defer os.RemoveAll("build_multi_test")
// Create source files
// File 1: Has FieldA, no Class.
// File 2: Has Class, FieldB.
// Both in package +MyObj
f1Content := `
#package Proj.+MyObj
FieldA = 10
`
f2Content := `
#package Proj.+MyObj
Class = "MyClass"
FieldB = 20
`
ioutil.WriteFile("build_multi_test/f1.marte", []byte(f1Content), 0644)
ioutil.WriteFile("build_multi_test/f2.marte", []byte(f2Content), 0644)
// Execute Build
b := builder.NewBuilder([]string{"build_multi_test/f1.marte", "build_multi_test/f2.marte"})
// Prepare output file
// Should be +MyObj.marte (normalized MyObj.marte) - Actually checking content
outputFile := "build_multi_test/MyObj.marte"
f, err := os.Create(outputFile)
if err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
defer f.Close()
err = b.Build(f)
if err != nil {
t.Fatalf("Build failed: %v", err)
}
f.Close() // Close to flush
// Check Output
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
t.Fatalf("Expected output file not found")
}
content, err := ioutil.ReadFile(outputFile)
if err != nil {
t.Fatalf("Failed to read output: %v", err)
}
output := string(content)
// Check presence
if !strings.Contains(output, "Class = \"MyClass\"") {
t.Error("Output missing Class")
}
if !strings.Contains(output, "FieldA = 10") {
t.Error("Output missing FieldA")
}
if !strings.Contains(output, "FieldB = 20") {
t.Error("Output missing FieldB")
}
// Check Order: Class/FieldB (from f2) should come BEFORE FieldA (from f1)
// because f2 has the Class definition.
idxClass := strings.Index(output, "Class")
idxFieldB := strings.Index(output, "FieldB")
idxFieldA := strings.Index(output, "FieldA")
if idxClass == -1 || idxFieldB == -1 || idxFieldA == -1 {
t.Fatal("Missing fields in output")
}
// Class should be first
if idxClass > idxFieldA {
t.Errorf("Expected Class (from f2) to be before FieldA (from f1). Output:\n%s", output)
}
// FieldB should be near Class (same fragment)
// FieldA should be after
if idxFieldB > idxFieldA {
t.Errorf("Expected FieldB (from f2) to be before FieldA (from f1). Output:\n%s", output)
}
}

View File

@@ -0,0 +1,6 @@
#package Proj.DupBase
+DupObj = {
Class = "DupClass"
FieldY = 1
}

View File

@@ -0,0 +1,3 @@
#package Proj.DupBase.DupObj
FieldY = 2

View File

@@ -0,0 +1,5 @@
#package Proj.Base
+MyObj = {
Class = "BaseClass"
}

View File

@@ -0,0 +1,3 @@
#package Proj.Base.MyObj
FieldX = 100

View File

@@ -0,0 +1,6 @@
#package Proj.TestPackage
+DupNode = {
Class = "DupClass"
FieldX = 1
}

View File

@@ -0,0 +1,5 @@
#package Proj.TestPackage
+DupNode = {
FieldX = 2
}

View File

@@ -0,0 +1,5 @@
#package Proj.TestPackage
+TargetNode = {
Class = "TargetClass"
}

View File

@@ -0,0 +1,6 @@
#package Proj.TestPackage
+SourceNode = {
Class = "SourceClass"
Target = TargetNode
}

View File

@@ -0,0 +1,5 @@
#package Proj.TestPackage
+MyNode = {
FieldA = 10
}

View File

@@ -0,0 +1,6 @@
#package Proj.TestPackage
+MyNode = {
Class = "MyClass"
FieldB = 20
}

View File

@@ -0,0 +1,196 @@
package integration
import (
"io/ioutil"
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func parseAndAddToIndex(t *testing.T, idx *index.ProjectTree, filePath string) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
t.Fatalf("Failed to read %s: %v", filePath, err)
}
p := parser.NewParser(string(content))
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed for %s: %v", filePath, err)
}
idx.AddFile(filePath, config)
}
func TestMultiFileNodeValidation(t *testing.T) {
idx := index.NewProjectTree()
parseAndAddToIndex(t, idx, "integration/multifile_valid_1.marte")
parseAndAddToIndex(t, idx, "integration/multifile_valid_2.marte")
// Resolving references might be needed if the validator relies on it for merging implicitly
// But primarily we want to check if the validator sees the merged node.
// The current implementation of Validator likely iterates over the ProjectTree.
// If the ProjectTree doesn't merge nodes automatically, the Validator needs to do it.
// However, the spec says "The build tool, validator, and LSP must merge these definitions".
// Let's assume the Validator or Index does the merging logic.
v := validator.NewValidator(idx)
v.ValidateProject()
// +MyNode is split.
// valid_1 has FieldA
// valid_2 has Class and FieldB
// If merging works, it should have a Class, so no error about missing Class.
for _, diag := range v.Diagnostics {
if strings.Contains(diag.Message, "must contain a 'Class' field") {
t.Errorf("Unexpected 'Class' field error for +MyNode: %s", diag.Message)
}
}
}
func TestMultiFileDuplicateField(t *testing.T) {
idx := index.NewProjectTree()
parseAndAddToIndex(t, idx, "integration/multifile_dup_1.marte")
parseAndAddToIndex(t, idx, "integration/multifile_dup_2.marte")
v := validator.NewValidator(idx)
v.ValidateProject()
foundError := false
for _, diag := range v.Diagnostics {
if strings.Contains(diag.Message, "Duplicate Field Definition") && strings.Contains(diag.Message, "FieldX") {
foundError = true
break
}
}
if !foundError {
t.Errorf("Expected duplicate field error for FieldX in +DupNode, but found none")
}
}
func TestMultiFileReference(t *testing.T) {
idx := index.NewProjectTree()
parseAndAddToIndex(t, idx, "integration/multifile_ref_1.marte")
parseAndAddToIndex(t, idx, "integration/multifile_ref_2.marte")
idx.ResolveReferences()
// Check if the reference in +SourceNode to TargetNode is resolved.
v := validator.NewValidator(idx)
v.ValidateProject()
if len(v.Diagnostics) > 0 {
// Filter out irrelevant errors
}
}
func TestHierarchicalPackageMerge(t *testing.T) {
idx := index.NewProjectTree()
parseAndAddToIndex(t, idx, "integration/hierarchical_pkg_1.marte")
parseAndAddToIndex(t, idx, "integration/hierarchical_pkg_2.marte")
v := validator.NewValidator(idx)
v.ValidateProject()
// +MyObj should have Class (from file 1) and FieldX (from file 2).
// If Class is missing, ValidateProject reports error.
for _, diag := range v.Diagnostics {
if strings.Contains(diag.Message, "must contain a 'Class' field") {
t.Errorf("Unexpected 'Class' field error for +MyObj: %s", diag.Message)
}
}
// We can also inspect the tree to verify FieldX is there (optional, but good for confidence)
baseNode := idx.Root.Children["Base"]
if baseNode == nil {
t.Fatal("Base node not found")
}
objNode := baseNode.Children["MyObj"]
if objNode == nil {
t.Fatal("MyObj node not found in Base")
}
hasFieldX := false
for _, frag := range objNode.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok && f.Name == "FieldX" {
hasFieldX = true
}
}
}
if !hasFieldX {
t.Error("FieldX not found in +MyObj")
}
}
func TestHierarchicalDuplicate(t *testing.T) {
idx := index.NewProjectTree()
parseAndAddToIndex(t, idx, "integration/hierarchical_dup_1.marte")
parseAndAddToIndex(t, idx, "integration/hierarchical_dup_2.marte")
v := validator.NewValidator(idx)
v.ValidateProject()
foundError := false
for _, diag := range v.Diagnostics {
if strings.Contains(diag.Message, "Duplicate Field Definition") && strings.Contains(diag.Message, "FieldY") {
foundError = true
break
}
}
if !foundError {
t.Errorf("Expected duplicate field error for FieldY in +DupObj (hierarchical), but found none")
}
}
func TestIsolatedFileValidation(t *testing.T) {
idx := index.NewProjectTree()
// File 1: Has package. Defines SharedClass.
f1Content := `
#package Proj.Pkg
+SharedObj = { Class = SharedClass }
`
p1 := parser.NewParser(f1Content)
c1, _ := p1.Parse()
idx.AddFile("shared.marte", c1)
// File 2: No package. References SharedObj.
// Should NOT resolve to SharedObj in shared.marte because iso.marte is isolated.
f2Content := `
+IsoObj = {
Class = "MyClass"
Ref = SharedObj
}
`
p2 := parser.NewParser(f2Content)
c2, _ := p2.Parse()
idx.AddFile("iso.marte", c2)
idx.ResolveReferences()
// Find reference
var ref *index.Reference
for i := range idx.References {
if idx.References[i].File == "iso.marte" && idx.References[i].Name == "SharedObj" {
ref = &idx.References[i]
break
}
}
if ref == nil {
t.Fatal("Reference SharedObj not found in index")
}
if ref.Target != nil {
t.Errorf("Expected reference in isolated file to be unresolved, but got target in %s", ref.Target.Fragments[0].File)
}
}

View File

@@ -0,0 +1,102 @@
package integration
import (
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestUnusedGAM(t *testing.T) {
content := `
+MyGAM = {
Class = GAMClass
+InputSignals = {}
}
+UsedGAM = {
Class = GAMClass
+InputSignals = {}
}
$App = {
$Data = {}
$States = {
$State = {
$Threads = {
$Thread = {
Functions = { UsedGAM }
}
}
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("test.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx)
v.CheckUnused()
foundUnused := false
for _, d := range v.Diagnostics {
if d.Message == "Unused GAM: +MyGAM is defined but not referenced in any thread or scheduler" {
foundUnused = true
break
}
}
if !foundUnused {
t.Error("Expected warning for unused GAM +MyGAM, but found none")
}
}
func TestUnusedSignal(t *testing.T) {
content := `
$App = {
$Data = {
+MyDS = {
Class = DataSourceClass
Sig1 = { Type = uint32 }
Sig2 = { Type = uint32 }
}
}
}
+MyGAM = {
Class = GAMClass
+InputSignals = {
S1 = { DataSource = MyDS Alias = Sig1 }
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("test.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx)
v.CheckUnused()
foundUnusedSig2 := false
for _, d := range v.Diagnostics {
if d.Message == "Unused Signal: Sig2 is defined in DataSource +MyDS but never referenced" {
foundUnusedSig2 = true
break
}
}
if !foundUnusedSig2 {
t.Error("Expected warning for unused signal Sig2, but found none")
}
}