Improving LSP
This commit is contained in:
@@ -86,7 +86,6 @@ func runCheck(args []string) {
|
|||||||
|
|
||||||
// Legacy loop removed as ValidateProject covers it via recursion
|
// Legacy loop removed as ValidateProject covers it via recursion
|
||||||
|
|
||||||
v.CheckUnused()
|
|
||||||
|
|
||||||
for _, diag := range v.Diagnostics {
|
for _, diag := range v.Diagnostics {
|
||||||
level := "ERROR"
|
level := "ERROR"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ var Tree = index.NewProjectTree()
|
|||||||
var Documents = make(map[string]string)
|
var Documents = make(map[string]string)
|
||||||
var ProjectRoot string
|
var ProjectRoot string
|
||||||
var GlobalSchema *schema.Schema
|
var GlobalSchema *schema.Schema
|
||||||
|
var Output io.Writer = os.Stdout
|
||||||
|
|
||||||
type JsonRpcMessage struct {
|
type JsonRpcMessage struct {
|
||||||
Jsonrpc string `json:"jsonrpc"`
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
@@ -404,7 +405,6 @@ func HandleFormatting(params DocumentFormattingParams) []TextEdit {
|
|||||||
func runValidation(_ string) {
|
func runValidation(_ string) {
|
||||||
v := validator.NewValidator(Tree, ProjectRoot)
|
v := validator.NewValidator(Tree, ProjectRoot)
|
||||||
v.ValidateProject()
|
v.ValidateProject()
|
||||||
v.CheckUnused()
|
|
||||||
|
|
||||||
// Group diagnostics by file
|
// Group diagnostics by file
|
||||||
fileDiags := make(map[string][]LSPDiagnostic)
|
fileDiags := make(map[string][]LSPDiagnostic)
|
||||||
@@ -646,7 +646,7 @@ func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
|
|||||||
|
|
||||||
dir := "NIL"
|
dir := "NIL"
|
||||||
if GlobalSchema != nil {
|
if GlobalSchema != nil {
|
||||||
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", cls))
|
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.#direction", cls))
|
||||||
val := GlobalSchema.Value.LookupPath(classPath)
|
val := GlobalSchema.Value.LookupPath(classPath)
|
||||||
if val.Err() == nil {
|
if val.Err() == nil {
|
||||||
var s string
|
var s string
|
||||||
@@ -1342,5 +1342,5 @@ func respond(id any, result any) {
|
|||||||
|
|
||||||
func send(msg any) {
|
func send(msg any) {
|
||||||
body, _ := json.Marshal(msg)
|
body, _ := json.Marshal(msg)
|
||||||
fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body)
|
fmt.Fprintf(Output, "Content-Length: %d\r\n\r\n%s", len(body), body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (l *Lexer) NextToken() Token {
|
|||||||
case '/':
|
case '/':
|
||||||
return l.lexComment()
|
return l.lexComment()
|
||||||
case '#':
|
case '#':
|
||||||
return l.lexPackage()
|
return l.lexHashIdentifier()
|
||||||
case '+':
|
case '+':
|
||||||
fallthrough
|
fallthrough
|
||||||
case '$':
|
case '$':
|
||||||
@@ -243,18 +243,19 @@ func (l *Lexer) lexUntilNewline(t TokenType) Token {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Lexer) lexPackage() Token {
|
func (l *Lexer) lexHashIdentifier() Token {
|
||||||
// We are at '#', l.start is just before it
|
// We are at '#', l.start is just before it
|
||||||
for {
|
for {
|
||||||
r := l.next()
|
r := l.next()
|
||||||
if unicode.IsLetter(r) {
|
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' || r == '#' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.backup()
|
l.backup()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if l.input[l.start:l.pos] == "#package" {
|
val := l.input[l.start:l.pos]
|
||||||
|
if val == "#package" {
|
||||||
return l.lexUntilNewline(TokenPackage)
|
return l.lexUntilNewline(TokenPackage)
|
||||||
}
|
}
|
||||||
return l.emit(TokenError)
|
return l.emit(TokenIdentifier)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ package schema
|
|||||||
...
|
...
|
||||||
}
|
}
|
||||||
TimingDataSource: {
|
TimingDataSource: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
IOGAM: {
|
IOGAM: {
|
||||||
@@ -66,71 +67,84 @@ package schema
|
|||||||
FileDataSource: {
|
FileDataSource: {
|
||||||
Filename: string
|
Filename: string
|
||||||
Format?: string
|
Format?: string
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
LoggerDataSource: {
|
LoggerDataSource: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
DANStream: {
|
DANStream: {
|
||||||
Timeout?: int
|
Timeout?: int
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
EPICSCAInput: {
|
EPICSCAInput: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
EPICSCAOutput: {
|
EPICSCAOutput: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
EPICSPVAInput: {
|
EPICSPVAInput: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
EPICSPVAOutput: {
|
EPICSPVAOutput: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
SDNSubscriber: {
|
SDNSubscriber: {
|
||||||
Address: string
|
Address: string
|
||||||
Port: int
|
Port: int
|
||||||
Interface?: string
|
Interface?: string
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
SDNPublisher: {
|
SDNPublisher: {
|
||||||
Address: string
|
Address: string
|
||||||
Port: int
|
Port: int
|
||||||
Interface?: string
|
Interface?: string
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
UDPReceiver: {
|
UDPReceiver: {
|
||||||
Port: int
|
Port: int
|
||||||
Address?: string
|
Address?: string
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
UDPSender: {
|
UDPSender: {
|
||||||
Destination: string
|
Destination: string
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
FileReader: {
|
FileReader: {
|
||||||
Filename: string
|
Filename: string
|
||||||
Format?: string
|
Format?: string
|
||||||
Interpolate?: string
|
Interpolate?: string
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
FileWriter: {
|
FileWriter: {
|
||||||
Filename: string
|
Filename: string
|
||||||
Format?: string
|
Format?: string
|
||||||
StoreOnTrigger?: int
|
StoreOnTrigger?: int
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
OrderedClass: {
|
OrderedClass: {
|
||||||
@@ -173,7 +187,8 @@ package schema
|
|||||||
TriggeredIOGAM: {...}
|
TriggeredIOGAM: {...}
|
||||||
WaveformGAM: {...}
|
WaveformGAM: {...}
|
||||||
DAN: {
|
DAN: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
LinuxTimer: {
|
LinuxTimer: {
|
||||||
@@ -184,11 +199,13 @@ package schema
|
|||||||
CPUMask?: int
|
CPUMask?: int
|
||||||
TimeProvider?: {...}
|
TimeProvider?: {...}
|
||||||
Signals: {...}
|
Signals: {...}
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
LinkDataSource: {
|
LinkDataSource: {
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
MDSReader: {
|
MDSReader: {
|
||||||
@@ -196,7 +213,8 @@ package schema
|
|||||||
ShotNumber: int
|
ShotNumber: int
|
||||||
Frequency: float | int
|
Frequency: float | int
|
||||||
Signals: {...}
|
Signals: {...}
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
MDSWriter: {
|
MDSWriter: {
|
||||||
@@ -212,57 +230,74 @@ package schema
|
|||||||
NumberOfPostTriggers?: int
|
NumberOfPostTriggers?: int
|
||||||
Signals: {...}
|
Signals: {...}
|
||||||
Messages?: {...}
|
Messages?: {...}
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI1588TimeStamp: {
|
NI1588TimeStamp: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI6259ADC: {
|
NI6259ADC: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI6259DAC: {
|
NI6259DAC: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI6259DIO: {
|
NI6259DIO: {
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI6368ADC: {
|
NI6368ADC: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI6368DAC: {
|
NI6368DAC: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI6368DIO: {
|
NI6368DIO: {
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI9157CircularFifoReader: {
|
NI9157CircularFifoReader: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
NI9157MxiDataSource: {
|
NI9157MxiDataSource: {
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
OPCUADSInput: {
|
OPCUADSInput: {
|
||||||
direction: "IN"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "IN"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
OPCUADSOutput: {
|
OPCUADSOutput: {
|
||||||
direction: "OUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "OUT"
|
||||||
|
...
|
||||||
|
}
|
||||||
|
RealTimeThreadAsyncBridge: {
|
||||||
|
#direction: "INOUT"
|
||||||
|
#multithreaded: bool | true
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
RealTimeThreadAsyncBridge: {...}
|
|
||||||
RealTimeThreadSynchronisation: {...}
|
RealTimeThreadSynchronisation: {...}
|
||||||
UARTDataSource: {
|
UARTDataSource: {
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
BaseLib2Wrapper: {...}
|
BaseLib2Wrapper: {...}
|
||||||
@@ -272,7 +307,8 @@ package schema
|
|||||||
OPCUA: {...}
|
OPCUA: {...}
|
||||||
SysLogger: {...}
|
SysLogger: {...}
|
||||||
GAMDataSource: {
|
GAMDataSource: {
|
||||||
direction: "INOUT"
|
#multithreaded: bool | *false
|
||||||
|
#direction: "INOUT"
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ func (v *Validator) ValidateProject() {
|
|||||||
for _, node := range v.Tree.IsolatedFiles {
|
for _, node := range v.Tree.IsolatedFiles {
|
||||||
v.validateNode(node)
|
v.validateNode(node)
|
||||||
}
|
}
|
||||||
|
v.CheckUnused()
|
||||||
|
v.CheckDataSourceThreading()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) validateNode(node *index.ProjectNode) {
|
func (v *Validator) validateNode(node *index.ProjectNode) {
|
||||||
@@ -314,7 +316,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
|
|||||||
if dsClass != "" {
|
if dsClass != "" {
|
||||||
// Lookup class definition in Schema
|
// Lookup class definition in Schema
|
||||||
// path: #Classes.ClassName.direction
|
// path: #Classes.ClassName.direction
|
||||||
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", dsClass))
|
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#direction", dsClass))
|
||||||
val := v.Schema.Value.LookupPath(path)
|
val := v.Schema.Value.LookupPath(path)
|
||||||
|
|
||||||
if val.Err() == nil {
|
if val.Err() == nil {
|
||||||
@@ -509,6 +511,8 @@ func (v *Validator) getFieldValue(f *parser.Field) string {
|
|||||||
return val.Raw
|
return val.Raw
|
||||||
case *parser.FloatValue:
|
case *parser.FloatValue:
|
||||||
return val.Raw
|
return val.Raw
|
||||||
|
case *parser.BoolValue:
|
||||||
|
return strconv.FormatBool(val.Value)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -741,3 +745,140 @@ func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bo
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckDataSourceThreading() {
|
||||||
|
if v.Tree.Root == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Find RealTimeApplication
|
||||||
|
var appNode *index.ProjectNode
|
||||||
|
findApp := func(n *index.ProjectNode) {
|
||||||
|
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
|
||||||
|
appNode = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Tree.Walk(findApp)
|
||||||
|
|
||||||
|
if appNode == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Find States
|
||||||
|
var statesNode *index.ProjectNode
|
||||||
|
if s, ok := appNode.Children["States"]; ok {
|
||||||
|
statesNode = s
|
||||||
|
} else {
|
||||||
|
for _, child := range appNode.Children {
|
||||||
|
if cls, ok := child.Metadata["Class"]; ok && cls == "StateMachine" {
|
||||||
|
statesNode = child
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statesNode == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Iterate States
|
||||||
|
for _, state := range statesNode.Children {
|
||||||
|
dsUsage := make(map[*index.ProjectNode]string) // DS Node -> Thread Name
|
||||||
|
var threads []*index.ProjectNode
|
||||||
|
|
||||||
|
// Search for threads in the state (either direct children or inside "Threads" container)
|
||||||
|
for _, child := range state.Children {
|
||||||
|
if child.RealName == "Threads" {
|
||||||
|
for _, t := range child.Children {
|
||||||
|
if cls, ok := t.Metadata["Class"]; ok && cls == "RealTimeThread" {
|
||||||
|
threads = append(threads, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if cls, ok := child.Metadata["Class"]; ok && cls == "RealTimeThread" {
|
||||||
|
threads = append(threads, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, thread := range threads {
|
||||||
|
gams := v.getThreadGAMs(thread)
|
||||||
|
for _, gam := range gams {
|
||||||
|
dss := v.getGAMDataSources(gam)
|
||||||
|
for _, ds := range dss {
|
||||||
|
if existingThread, ok := dsUsage[ds]; ok {
|
||||||
|
if existingThread != thread.RealName {
|
||||||
|
if !v.isMultithreaded(ds) {
|
||||||
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
|
Level: LevelError,
|
||||||
|
Message: fmt.Sprintf("DataSource '%s' is not multithreaded but used in multiple threads (%s, %s) in state '%s'", ds.RealName, existingThread, thread.RealName, state.RealName),
|
||||||
|
Position: v.getNodePosition(gam),
|
||||||
|
File: v.getNodeFile(gam),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dsUsage[ds] = thread.RealName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) getThreadGAMs(thread *index.ProjectNode) []*index.ProjectNode {
|
||||||
|
var gams []*index.ProjectNode
|
||||||
|
fields := v.getFields(thread)
|
||||||
|
if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 {
|
||||||
|
f := funcs[0]
|
||||||
|
if arr, ok := f.Value.(*parser.ArrayValue); ok {
|
||||||
|
for _, elem := range arr.Elements {
|
||||||
|
if ref, ok := elem.(*parser.ReferenceValue); ok {
|
||||||
|
target := v.resolveReference(ref.Value, v.getNodeFile(thread), isGAM)
|
||||||
|
if target != nil {
|
||||||
|
gams = append(gams, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gams
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNode {
|
||||||
|
dsMap := make(map[*index.ProjectNode]bool)
|
||||||
|
|
||||||
|
processSignals := func(container *index.ProjectNode) {
|
||||||
|
if container == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, sig := range container.Children {
|
||||||
|
fields := v.getFields(sig)
|
||||||
|
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
||||||
|
dsName := v.getFieldValue(dsFields[0])
|
||||||
|
dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
|
||||||
|
if dsNode != nil {
|
||||||
|
dsMap[dsNode] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processSignals(gam.Children["InputSignals"])
|
||||||
|
processSignals(gam.Children["OutputSignals"])
|
||||||
|
|
||||||
|
var dss []*index.ProjectNode
|
||||||
|
for ds := range dsMap {
|
||||||
|
dss = append(dss, ds)
|
||||||
|
}
|
||||||
|
return dss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
|
||||||
|
fields := v.getFields(ds)
|
||||||
|
if mt, ok := fields["#multithreaded"]; ok && len(mt) > 0 {
|
||||||
|
val := v.getFieldValue(mt[0])
|
||||||
|
return val == "true"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestSuggestSignalsRobustness(t *testing.T) {
|
|||||||
custom := []byte(`
|
custom := []byte(`
|
||||||
package schema
|
package schema
|
||||||
#Classes: {
|
#Classes: {
|
||||||
InOutReader: { direction: "INOUT" }
|
InOutReader: { #direction: "INOUT" }
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
val := lsp.GlobalSchema.Context.CompileBytes(custom)
|
val := lsp.GlobalSchema.Context.CompileBytes(custom)
|
||||||
|
|||||||
75
test/lsp_validation_threading_test.go
Normal file
75
test/lsp_validation_threading_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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 TestLSPValidationThreading(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
lsp.Tree = index.NewProjectTree()
|
||||||
|
lsp.Documents = make(map[string]string)
|
||||||
|
lsp.ProjectRoot = "."
|
||||||
|
lsp.GlobalSchema = schema.NewSchema() // Empty schema but not nil
|
||||||
|
|
||||||
|
// Capture Output
|
||||||
|
var buf bytes.Buffer
|
||||||
|
lsp.Output = &buf
|
||||||
|
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+SharedDS = {
|
||||||
|
Class = GAMDataSource
|
||||||
|
#direction = "INOUT"
|
||||||
|
#multithreaded = false
|
||||||
|
Signals = {
|
||||||
|
Sig1 = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+GAM1 = { Class = IOGAM InputSignals = { Sig1 = { DataSource = SharedDS Type = uint32 } } }
|
||||||
|
+GAM2 = { Class = IOGAM OutputSignals = { Sig1 = { DataSource = SharedDS Type = uint32 } } }
|
||||||
|
+App = {
|
||||||
|
Class = RealTimeApplication
|
||||||
|
+States = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+State1 = {
|
||||||
|
Class = RealTimeState
|
||||||
|
+Thread1 = { Class = RealTimeThread Functions = { GAM1 } }
|
||||||
|
+Thread2 = { Class = RealTimeThread Functions = { GAM2 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
uri := "file://threading.marte"
|
||||||
|
|
||||||
|
// Call HandleDidOpen directly
|
||||||
|
params := lsp.DidOpenTextDocumentParams{
|
||||||
|
TextDocument: lsp.TextDocumentItem{
|
||||||
|
URI: uri,
|
||||||
|
Text: content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lsp.HandleDidOpen(params)
|
||||||
|
|
||||||
|
// Check output
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
// We look for publishDiagnostics notification
|
||||||
|
if !strings.Contains(output, "textDocument/publishDiagnostics") {
|
||||||
|
t.Fatal("Did not receive publishDiagnostics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We look for the specific error message
|
||||||
|
expectedError := "DataSource '+SharedDS' is not multithreaded but used in multiple threads"
|
||||||
|
if !strings.Contains(output, expectedError) {
|
||||||
|
t.Errorf("Expected error '%s' not found in LSP output. Output:\n%s", expectedError, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
test/validator_datasource_threading_test.go
Normal file
120
test/validator_datasource_threading_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDataSourceThreadingValidation(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+SharedDS = {
|
||||||
|
Class = GAMDataSource
|
||||||
|
#direction = "INOUT"
|
||||||
|
#multithreaded = false
|
||||||
|
Signals = {
|
||||||
|
Sig1 = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+MultiDS = {
|
||||||
|
Class = GAMDataSource
|
||||||
|
#direction = "INOUT"
|
||||||
|
#multithreaded = true
|
||||||
|
Signals = {
|
||||||
|
Sig1 = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+GAM1 = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
Sig1 = { DataSource = SharedDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+GAM2 = {
|
||||||
|
Class = IOGAM
|
||||||
|
OutputSignals = {
|
||||||
|
Sig1 = { DataSource = SharedDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+GAM3 = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
Sig1 = { DataSource = MultiDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+GAM4 = {
|
||||||
|
Class = IOGAM
|
||||||
|
OutputSignals = {
|
||||||
|
Sig1 = { DataSource = MultiDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+App = {
|
||||||
|
Class = RealTimeApplication
|
||||||
|
+States = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+State1 = {
|
||||||
|
Class = RealTimeState
|
||||||
|
+Thread1 = {
|
||||||
|
Class = RealTimeThread
|
||||||
|
Functions = { GAM1 }
|
||||||
|
}
|
||||||
|
+Thread2 = {
|
||||||
|
Class = RealTimeThread
|
||||||
|
Functions = { GAM2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+State2 = {
|
||||||
|
Class = RealTimeState
|
||||||
|
+Thread1 = {
|
||||||
|
Class = RealTimeThread
|
||||||
|
Functions = { GAM3 }
|
||||||
|
}
|
||||||
|
+Thread2 = {
|
||||||
|
Class = RealTimeThread
|
||||||
|
Functions = { GAM4 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
pt := index.NewProjectTree()
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
cfg, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pt.AddFile("main.marte", cfg)
|
||||||
|
|
||||||
|
// Since we don't load schema here (empty path), it won't validate classes via CUE,
|
||||||
|
// but CheckDataSourceThreading relies on parsing logic, not CUE schema unification.
|
||||||
|
// So it should work.
|
||||||
|
|
||||||
|
v := validator.NewValidator(pt, "")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundError := false
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "not multithreaded but used in multiple threads") {
|
||||||
|
if strings.Contains(d.Message, "SharedDS") {
|
||||||
|
foundError = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "MultiDS") {
|
||||||
|
t.Error("Unexpected threading error for MultiDS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundError {
|
||||||
|
t.Error("Expected threading error for SharedDS")
|
||||||
|
// Debug
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
t.Logf("Diag: %s", d.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ func TestGAMSignalLinking(t *testing.T) {
|
|||||||
|
|
||||||
+MyGAM = {
|
+MyGAM = {
|
||||||
Class = IOGAM
|
Class = IOGAM
|
||||||
|
//! ignore(unused)
|
||||||
InputSignals = {
|
InputSignals = {
|
||||||
MySig = {
|
MySig = {
|
||||||
DataSource = MyDS
|
DataSource = MyDS
|
||||||
|
|||||||
Reference in New Issue
Block a user