Implemented more robust LSP diagnostics and better parsing logic
This commit is contained in:
101
test/lsp_fuzz_test.go
Normal file
101
test/lsp_fuzz_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
)
|
||||
|
||||
func TestIncrementalFuzz(t *testing.T) {
|
||||
// Initialize
|
||||
lsp.Documents = make(map[string]string)
|
||||
uri := "file://fuzz.marte"
|
||||
currentText := ""
|
||||
lsp.Documents[uri] = currentText
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Apply 1000 random edits
|
||||
for i := 0; i < 1000; i++ {
|
||||
// Randomly choose Insert or Delete
|
||||
isInsert := rand.Intn(2) == 0
|
||||
|
||||
change := lsp.TextDocumentContentChangeEvent{}
|
||||
|
||||
// Use simple ascii string
|
||||
length := len(currentText)
|
||||
|
||||
if isInsert || length == 0 {
|
||||
// Insert
|
||||
pos := 0
|
||||
if length > 0 {
|
||||
pos = rand.Intn(length + 1)
|
||||
}
|
||||
|
||||
insertStr := "X"
|
||||
if rand.Intn(5) == 0 { insertStr = "\n" }
|
||||
if rand.Intn(10) == 0 { insertStr = "longstring" }
|
||||
|
||||
// Calculate Line/Char for pos
|
||||
line, char := offsetToLineChar(currentText, pos)
|
||||
|
||||
change.Range = &lsp.Range{
|
||||
Start: lsp.Position{Line: line, Character: char},
|
||||
End: lsp.Position{Line: line, Character: char},
|
||||
}
|
||||
change.Text = insertStr
|
||||
|
||||
// Expected
|
||||
currentText = currentText[:pos] + insertStr + currentText[pos:]
|
||||
} else {
|
||||
// Delete
|
||||
start := rand.Intn(length)
|
||||
end := start + 1 + rand.Intn(length - start) // at least 1 char
|
||||
|
||||
// Range
|
||||
l1, c1 := offsetToLineChar(currentText, start)
|
||||
l2, c2 := offsetToLineChar(currentText, end)
|
||||
|
||||
change.Range = &lsp.Range{
|
||||
Start: lsp.Position{Line: l1, Character: c1},
|
||||
End: lsp.Position{Line: l2, Character: c2},
|
||||
}
|
||||
change.Text = ""
|
||||
|
||||
currentText = currentText[:start] + currentText[end:]
|
||||
}
|
||||
|
||||
// Apply
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: i},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change},
|
||||
})
|
||||
|
||||
// Verify
|
||||
if lsp.Documents[uri] != currentText {
|
||||
t.Fatalf("Fuzz iteration %d failed.\nExpected len: %d\nGot len: %d\nChange: %+v", i, len(currentText), len(lsp.Documents[uri]), change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func offsetToLineChar(text string, offset int) (int, int) {
|
||||
line := 0
|
||||
char := 0
|
||||
for i, r := range text {
|
||||
if i == offset {
|
||||
return line, char
|
||||
}
|
||||
if r == '\n' {
|
||||
line++
|
||||
char = 0
|
||||
} else {
|
||||
char++
|
||||
}
|
||||
}
|
||||
if offset == len(text) {
|
||||
return line, char
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
204
test/lsp_incremental_correctness_test.go
Normal file
204
test/lsp_incremental_correctness_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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/schema"
|
||||
)
|
||||
|
||||
func TestIncrementalCorrectness(t *testing.T) {
|
||||
lsp.Documents = make(map[string]string)
|
||||
uri := "file://test.txt"
|
||||
initial := "12345\n67890"
|
||||
lsp.Documents[uri] = initial
|
||||
|
||||
// Edit 1: Insert "A" at 0:1 -> "1A2345\n67890"
|
||||
change1 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 1}, End: lsp.Position{Line: 0, Character: 1}},
|
||||
Text: "A",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change1},
|
||||
})
|
||||
|
||||
if lsp.Documents[uri] != "1A2345\n67890" {
|
||||
t.Errorf("Edit 1 failed: %q", lsp.Documents[uri])
|
||||
}
|
||||
|
||||
// Edit 2: Delete newline (merge lines)
|
||||
// "1A2345\n67890" -> "1A234567890"
|
||||
// \n is at index 6.
|
||||
// 0:6 points to \n? "1A2345" length is 6.
|
||||
// So 0:6 is AFTER '5', at '\n'.
|
||||
// 1:0 is AFTER '\n', at '6'.
|
||||
// Range 0:6 - 1:0 covers '\n'.
|
||||
change2 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 6}, End: lsp.Position{Line: 1, Character: 0}},
|
||||
Text: "",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change2},
|
||||
})
|
||||
|
||||
if lsp.Documents[uri] != "1A234567890" {
|
||||
t.Errorf("Edit 2 failed: %q", lsp.Documents[uri])
|
||||
}
|
||||
|
||||
// Edit 3: Add newline at end
|
||||
// "1A234567890" len 11.
|
||||
// 0:11.
|
||||
change3 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 11}, End: lsp.Position{Line: 0, Character: 11}},
|
||||
Text: "\n",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change3},
|
||||
})
|
||||
|
||||
if lsp.Documents[uri] != "1A234567890\n" {
|
||||
t.Errorf("Edit 3 failed: %q", lsp.Documents[uri])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementalAppValidation(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".")
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := `// Test app
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+TimingDataSource = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+A = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
A = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
// Placeholder
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
B = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State = {
|
||||
Class =RealTimeState
|
||||
Threads = {
|
||||
+Th1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = {A}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = TimingDataSource
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://app_inc.marte"
|
||||
|
||||
// 1. Open
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
|
||||
})
|
||||
|
||||
out := buf.String()
|
||||
|
||||
// Signal A is never produced. Should have consumed error.
|
||||
if !strings.Contains(out, "ERROR: INOUT Signal 'A'") {
|
||||
t.Error("Missing consumed error for A")
|
||||
}
|
||||
// Signal B is Output, never consumed.
|
||||
if !strings.Contains(out, "WARNING: INOUT Signal 'B'") {
|
||||
t.Error("Missing produced error for B")
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// 2. Insert comment at start
|
||||
// Expecting same errors
|
||||
change1 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 0}, End: lsp.Position{Line: 0, Character: 0}},
|
||||
Text: "// Comment\n",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change1},
|
||||
})
|
||||
|
||||
out = buf.String()
|
||||
// Signal A is never produced. Should have consumed error.
|
||||
if !strings.Contains(out, "ERROR: INOUT Signal 'A'") {
|
||||
t.Error("Missing consumed error for A")
|
||||
}
|
||||
// Signal B is Output, never consumed.
|
||||
if !strings.Contains(out, "WARNING: INOUT Signal 'B'") {
|
||||
t.Error("Missing produced error for B")
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// 3. Add Value to A
|
||||
currentText := lsp.Documents[uri]
|
||||
idx := strings.Index(currentText, "Placeholder")
|
||||
if idx == -1 {
|
||||
t.Fatal("Could not find anchor string")
|
||||
}
|
||||
|
||||
idx = strings.Index(currentText[idx:], "\n") + idx
|
||||
insertPos := idx + 1
|
||||
|
||||
line, char := offsetToLineChar(currentText, insertPos)
|
||||
|
||||
change2 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: line, Character: char}, End: lsp.Position{Line: line, Character: char}},
|
||||
Text: "Value = 10\n",
|
||||
}
|
||||
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change2},
|
||||
})
|
||||
|
||||
out = buf.String()
|
||||
|
||||
// Signal A has now a Value field and so it is produced. Should NOT have consumed error.
|
||||
if strings.Contains(out, "ERROR: INOUT Signal 'A'") {
|
||||
t.Error("Unexpected consumed error for A")
|
||||
}
|
||||
// Signal B is Output, never consumed.
|
||||
if !strings.Contains(out, "WARNING: INOUT Signal 'B'") {
|
||||
t.Error("Missing produced error for B")
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user