Compare commits

..

3 Commits

Author SHA1 Message Date
Martino Ferrari
31996ae710 minor improvement on the cue schema validator 2026-01-28 01:18:26 +01:00
Martino Ferrari
776b1fddc3 removed project node from output 2026-01-28 01:18:09 +01:00
Martino Ferrari
597fd3eddf improved sdnpublisher schema 2026-01-28 00:07:10 +01:00
4 changed files with 253 additions and 49 deletions

View File

@@ -56,27 +56,44 @@ func (b *Builder) Build(f *os.File) error {
tree.AddFile(file, config) tree.AddFile(file, config)
} }
// Determine root node to print
rootNode := tree.Root
if expectedProject != "" {
if child, ok := tree.Root.Children[expectedProject]; ok {
rootNode = child
}
}
// Write entire root content (definitions and children) to the single output file // Write entire root content (definitions and children) to the single output file
b.writeNodeContent(f, tree.Root, 0) b.writeNodeBody(f, rootNode, 0)
return nil return nil
} }
func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent int) { func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent int) {
// 1. Sort Fragments: Class first
sort.SliceStable(node.Fragments, func(i, j int) bool {
return hasClass(node.Fragments[i]) && !hasClass(node.Fragments[j])
})
indentStr := strings.Repeat(" ", indent) indentStr := strings.Repeat(" ", indent)
// If this node has a RealName (e.g. +App), we print it as an object definition // If this node has a RealName (e.g. +App), we print it as an object definition
if node.RealName != "" { if node.RealName != "" {
fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName) fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName)
indent++ indent++
indentStr = strings.Repeat(" ", indent)
} }
b.writeNodeBody(f, node, indent)
if node.RealName != "" {
indent--
indentStr = strings.Repeat(" ", indent)
fmt.Fprintf(f, "%s}\n", indentStr)
}
}
func (b *Builder) writeNodeBody(f *os.File, node *index.ProjectNode, indent int) {
// 1. Sort Fragments: Class first
sort.SliceStable(node.Fragments, func(i, j int) bool {
return hasClass(node.Fragments[i]) && !hasClass(node.Fragments[j])
})
writtenChildren := make(map[string]bool) writtenChildren := make(map[string]bool)
// 2. Write definitions from fragments // 2. Write definitions from fragments
@@ -110,12 +127,6 @@ func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent i
child := node.Children[k] child := node.Children[k]
b.writeNodeContent(f, child, indent) b.writeNodeContent(f, child, indent)
} }
if node.RealName != "" {
indent--
indentStr = strings.Repeat(" ", indent)
fmt.Fprintf(f, "%s}\n", indentStr)
}
} }
func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int) { func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int) {

View File

@@ -2,9 +2,32 @@ package schema
#Classes: { #Classes: {
RealTimeApplication: { RealTimeApplication: {
Functions: {...} // type: node Functions!: {
Data!: {...} // type: node Class: "ReferenceContainer"
States!: {...} // type: node [_= !~"^Class$"]: {
#meta: type: "gam"
...
}
} // type: node
Data!: {
Class: "ReferenceContainer"
DefaultDataSource: string
[_= !~"^(Class|DefaultDataSource)$"]: {
#meta: type: "datasource"
...
}
}
States!: {
Class: "ReferenceContainer"
[_= !~"^Class$"]: {
Class: "RealTimeState"
...
}
} // type: node
Scheduler!: {
...
#meta: type: "scheduler"
}
... ...
} }
Message: { Message: {
@@ -23,7 +46,7 @@ package schema
Class: "ReferenceContainer" Class: "ReferenceContainer"
... ...
} }
[_ = !~"^(Class|ENTER)$"]: StateMachineEvent [_ = !~"^(Class|ENTER|EXIT)$"]: StateMachineEvent
... ...
} }
StateMachine: { StateMachine: {
@@ -40,16 +63,19 @@ package schema
} }
GAMScheduler: { GAMScheduler: {
TimingDataSource: string // type: reference TimingDataSource: string // type: reference
#meta: type: "scheduler"
... ...
} }
TimingDataSource: { TimingDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
IOGAM: { IOGAM: {
InputSignals?: {...} // type: node InputSignals?: {...} // type: node
OutputSignals?: {...} // type: node OutputSignals?: {...} // type: node
#meta: type: "gam"
... ...
} }
ReferenceContainer: { ReferenceContainer: {
@@ -57,94 +83,114 @@ package schema
} }
ConstantGAM: { ConstantGAM: {
... ...
#meta: type: "gam"
} }
PIDGAM: { PIDGAM: {
Kp: float | int // type: float (allow int as it promotes) Kp: float | int // type: float (allow int as it promotes)
Ki: float | int Ki: float | int
Kd: float | int Kd: float | int
#meta: type: "gam"
... ...
} }
FileDataSource: { FileDataSource: {
Filename: string Filename: string
Format?: string Format?: string
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
LoggerDataSource: { LoggerDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
DANStream: { DANStream: {
Timeout?: int Timeout?: int
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
EPICSCAInput: { EPICSCAInput: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
EPICSCAOutput: { EPICSCAOutput: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
EPICSPVAInput: { EPICSPVAInput: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
EPICSPVAOutput: { EPICSPVAOutput: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
SDNSubscriber: { SDNSubscriber: {
Address: string ExecutionMode?: *"IndependentThread" | "RealTimeThread"
Port: int Topic!: string
Interface?: string Address?: string
Interface!: string
CPUs?: uint32
InternalTimeout?: uint32
Timeout?: uint32
IgnoreTimeoutError?: 0 | 1
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
SDNPublisher: { SDNPublisher: {
Address: string Address: string
Port: int Port: int
Interface?: string Interface?: string
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
UDPReceiver: { UDPReceiver: {
Port: int Port: int
Address?: string Address?: string
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
UDPSender: { UDPSender: {
Destination: string Destination: string
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
FileReader: { FileReader: {
Filename: string Filename: string
Format?: string Format?: string
Interpolate?: string Interpolate?: string
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
FileWriter: { FileWriter: {
Filename: string Filename: string
Format?: string Format?: string
StoreOnTrigger?: int StoreOnTrigger?: int
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
OrderedClass: { OrderedClass: {
@@ -152,15 +198,25 @@ package schema
Second: string Second: string
... ...
} }
BaseLib2GAM: {...} BaseLib2GAM: {
ConversionGAM: {...} #meta: type: "gam"
DoubleHandshakeGAM: {...} ...
}
ConversionGAM: {
#meta: type: "gam"
...
}
DoubleHandshakeGAM: {
#meta: type: "gam"
...
}
FilterGAM: { FilterGAM: {
Num: [...] Num: [...]
Den: [...] Den: [...]
ResetInEachState?: _ ResetInEachState?: _
InputSignals?: {...} InputSignals?: {...}
OutputSignals?: {...} OutputSignals?: {...}
#meta: type: "gam"
... ...
} }
HistogramGAM: { HistogramGAM: {
@@ -168,24 +224,57 @@ package schema
StateChangeResetName?: string StateChangeResetName?: string
InputSignals?: {...} InputSignals?: {...}
OutputSignals?: {...} OutputSignals?: {...}
#meta: type: "gam"
...
}
Interleaved2FlatGAM: {
#meta: type: "gam"
...
}
FlattenedStructIOGAM: {
#meta: type: "gam"
... ...
} }
Interleaved2FlatGAM: {...}
FlattenedStructIOGAM: {...}
MathExpressionGAM: { MathExpressionGAM: {
Expression: string Expression: string
InputSignals?: {...} InputSignals?: {...}
OutputSignals?: {...} OutputSignals?: {...}
#meta: type: "gam"
...
}
MessageGAM: {
#meta: type: "gam"
...
}
MuxGAM: {
#meta: type: "gam"
...
}
SimulinkWrapperGAM: {
#meta: type: "gam"
...
}
SSMGAM: {
#meta: type: "gam"
...
}
StatisticsGAM: {
#meta: type: "gam"
...
}
TimeCorrectionGAM: {
#meta: type: "gam"
...
}
TriggeredIOGAM: {
#meta: type: "gam"
...
}
WaveformGAM: {
#meta: type: "gam"
... ...
} }
MessageGAM: {...}
MuxGAM: {...}
SimulinkWrapperGAM: {...}
SSMGAM: {...}
StatisticsGAM: {...}
TimeCorrectionGAM: {...}
TriggeredIOGAM: {...}
WaveformGAM: {...}
DAN: { DAN: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
@@ -201,11 +290,13 @@ package schema
Signals: {...} Signals: {...}
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
LinkDataSource: { LinkDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
MDSReader: { MDSReader: {
@@ -215,6 +306,7 @@ package schema
Signals: {...} Signals: {...}
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
MDSWriter: { MDSWriter: {
@@ -232,72 +324,86 @@ package schema
Messages?: {...} Messages?: {...}
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
NI1588TimeStamp: { NI1588TimeStamp: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI6259ADC: { NI6259ADC: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI6259DAC: { NI6259DAC: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
NI6259DIO: { NI6259DIO: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
NI6368ADC: { NI6368ADC: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI6368DAC: { NI6368DAC: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
NI6368DIO: { NI6368DIO: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
NI9157CircularFifoReader: { NI9157CircularFifoReader: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI9157MxiDataSource: { NI9157MxiDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
OPCUADSInput: { OPCUADSInput: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "IN" #meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
OPCUADSOutput: { OPCUADSOutput: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "OUT" #meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
RealTimeThreadAsyncBridge: { RealTimeThreadAsyncBridge: {
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: multithreaded: bool | true #meta: multithreaded: bool | true
#meta: type: "datasource"
... ...
} }
RealTimeThreadSynchronisation: {...} RealTimeThreadSynchronisation: {...}
UARTDataSource: { UARTDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
BaseLib2Wrapper: {...} BaseLib2Wrapper: {...}
@@ -309,15 +415,23 @@ package schema
GAMDataSource: { GAMDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: bool | *false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
} }
#Meta: {
direction?: "IN" | "OUT" | "INOUT"
multithreaded?: bool
...
}
// Definition for any Object. // Definition for any Object.
// It must have a Class field. // It must have a Class field.
// Based on Class, it validates against #Classes. // Based on Class, it validates against #Classes.
#Object: { #Object: {
Class: string Class: string
"#meta"?: #Meta
// Allow any other field by default (extensibility), // Allow any other field by default (extensibility),
// unless #Classes definition is closed. // unless #Classes definition is closed.
// We allow open structs now. // We allow open structs now.

View File

@@ -15,7 +15,7 @@ func TestSDNSubscriberValidation(t *testing.T) {
+MySDN = { +MySDN = {
Class = SDNSubscriber Class = SDNSubscriber
Address = "239.0.0.1" Address = "239.0.0.1"
// Missing Port // Missing Interface
} }
` `
p := parser.NewParser(content) p := parser.NewParser(content)
@@ -32,7 +32,7 @@ func TestSDNSubscriberValidation(t *testing.T) {
found := false found := false
for _, d := range v.Diagnostics { for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Port: incomplete value") { if strings.Contains(d.Message, "Interface: field is required but not present") {
found = true found = true
break break
} }

View File

@@ -0,0 +1,79 @@
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 TestSchemaMetaValidation(t *testing.T) {
// 1. Valid Usage
validContent := `
+App = {
Class = RealTimeApplication
Functions = { Class = ReferenceContainer }
Data = { Class = ReferenceContainer DefaultDataSource = "DS" }
States = { Class = ReferenceContainer }
Scheduler = { Class = GAMScheduler TimingDataSource = "DS" }
#meta = {
multithreaded = true
}
}
`
pt := index.NewProjectTree()
p := parser.NewParser(validContent)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
pt.AddFile("valid.marte", cfg)
v := validator.NewValidator(pt, "")
v.ValidateProject()
if len(v.Diagnostics) > 0 {
for _, d := range v.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
t.Errorf("Expected no errors for valid #meta")
}
// 2. Invalid Usage (Wrong Type)
invalidContent := `
+App = {
Class = RealTimeApplication
Functions = { Class = ReferenceContainer }
Data = { Class = ReferenceContainer DefaultDataSource = "DS" }
States = { Class = ReferenceContainer }
Scheduler = { Class = GAMScheduler TimingDataSource = "DS" }
#meta = {
multithreaded = "yes" // Should be bool
}
}
`
pt2 := index.NewProjectTree()
p2 := parser.NewParser(invalidContent)
cfg2, _ := p2.Parse()
pt2.AddFile("invalid.marte", cfg2)
v2 := validator.NewValidator(pt2, "")
v2.ValidateProject()
foundError := false
for _, d := range v2.Diagnostics {
// CUE validation error message
if strings.Contains(d.Message, "mismatched types") || strings.Contains(d.Message, "conflicting values") {
foundError = true
}
}
if !foundError {
t.Error("Expected error for invalid #meta type, got nothing")
for _, d := range v2.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
}
}