Compare commits
71 Commits
1ea518a58a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae701e8c1 | ||
|
|
23ddbc0e91 | ||
|
|
ee9235c24d | ||
|
|
749eab0a32 | ||
|
|
12615aa6d2 | ||
|
|
bd845aa859 | ||
|
|
b879766021 | ||
|
|
d2b2750833 | ||
|
|
55ca313b73 | ||
|
|
ff19fef779 | ||
|
|
d4075ff809 | ||
|
|
f121f7c15d | ||
|
|
b4d3edab9d | ||
|
|
ee9674a7bc | ||
|
|
d98593e67b | ||
|
|
a55c4b9c7c | ||
|
|
6fa67abcb4 | ||
|
|
c3f4d8f465 | ||
|
|
0cbbf5939a | ||
|
|
ecc7039306 | ||
|
|
2fd6d3d096 | ||
|
|
2e25c8ff11 | ||
|
|
8be139ab27 | ||
|
|
cb79d490e7 | ||
|
|
b8d45f276d | ||
|
|
03fe7d33b0 | ||
|
|
8811ac9273 | ||
|
|
71c86f1dcb | ||
|
|
ab22a939d7 | ||
|
|
01bcd66594 | ||
|
|
31996ae710 | ||
|
|
776b1fddc3 | ||
|
|
597fd3eddf | ||
|
|
6781d50ee4 | ||
|
|
1d7dc665d6 | ||
|
|
4ea406a17b | ||
|
|
fed39467fd | ||
|
|
15afdc91f4 | ||
|
|
213fc81cfb | ||
|
|
71a3c40108 | ||
|
|
aedc715ef3 | ||
|
|
73cfc43f4b | ||
|
|
599beb6f4f | ||
|
|
30a105df63 | ||
|
|
04196d8a1f | ||
|
|
02274f1bbf | ||
|
|
12ed4cfbd2 | ||
|
|
bbeb344d19 | ||
|
|
eeb4f5da2e | ||
|
|
8e13020d50 | ||
|
|
c9cc67f663 | ||
|
|
0ffcecf19e | ||
|
|
761cf83b8e | ||
|
|
7caf3a5da5 | ||
|
|
94ee7e4880 | ||
|
|
ce9b68200e | ||
|
|
e3c84fcf60 | ||
|
|
4a515fd6c3 | ||
|
|
14cba1b530 | ||
|
|
462c832651 | ||
|
|
77fe3e9cac | ||
|
|
0ee44c0a27 | ||
|
|
d450d358b4 | ||
|
|
2cdcfe2812 | ||
|
|
ef7729475a | ||
|
|
99bd5bffdd | ||
|
|
4379960835 | ||
|
|
2aeec1e5f6 | ||
|
|
5853365707 | ||
|
|
5c3f05a1a4 | ||
|
|
e2c87c90f3 |
28
.gitignore
vendored
28
.gitignore
vendored
@@ -1,2 +1,30 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# build folder
|
||||
build
|
||||
# log output
|
||||
*.log
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Martino G. Ferrari <manda.mgf@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
30
Makefile
Normal file
30
Makefile
Normal file
@@ -0,0 +1,30 @@
|
||||
BINARY_NAME=mdt
|
||||
BUILD_DIR=build
|
||||
|
||||
.PHONY: all build test coverage clean install vet fmt
|
||||
|
||||
all: vet test build
|
||||
|
||||
build:
|
||||
mkdir -p $(BUILD_DIR)
|
||||
go build -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/mdt
|
||||
|
||||
test:
|
||||
go test -v ./test/...
|
||||
|
||||
coverage:
|
||||
go test -cover -coverprofile=coverage.out ./test/... -coverpkg=./internal/...
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -f coverage.out
|
||||
|
||||
install:
|
||||
go install ./cmd/mdt
|
||||
131
README.md
Normal file
131
README.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# MARTe Development Tools (mdt)
|
||||
|
||||
`mdt` is a comprehensive toolkit for developing, validating, and building configurations for the MARTe real-time framework. It provides a CLI and a Language Server Protocol (LSP) server to enhance the development experience.
|
||||
|
||||
## Features
|
||||
|
||||
- **Portability**: A single statically compiled executable compatible with any Linux 3.2+ machine (as well as possible to compile and run on Windows and Mac OS X)
|
||||
- **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, navigation (Go to Definition/References), and Inlay Hints (inline types and evaluation).
|
||||
- **Builder**: Merges multiple configuration files into a single, ordered output file.
|
||||
- **Formatter**: Standardizes configuration file formatting.
|
||||
- **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness.
|
||||
|
||||
### MARTe extended configuration language
|
||||
|
||||
Few additional features have been added to the standard MARTe configuration language:
|
||||
|
||||
- Multi file configuration support
|
||||
- Multi file definition merging
|
||||
- File level namespace / node (`#package`)
|
||||
- Variables and Constants
|
||||
- Overrideable variables (`#var`)
|
||||
- Fixed constants (`#let`)
|
||||
- Powerful expressions (arithmetic, bitwise, string concatenation)
|
||||
- Doc-strings support (`//#`) for objects, fields, and variables
|
||||
- Pragmas (`//!`) for warning suppression / documentation
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Step-by-Step Tutorial](docs/TUTORIAL.md)
|
||||
- [Editor Integration Guide](docs/EDITOR_INTEGRATION.md)
|
||||
- [Configuration Guide](docs/CONFIGURATION_GUIDE.md)
|
||||
- [Examples Readme](/examples/README.md)
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
Requirements: Go 1.21+
|
||||
|
||||
```bash
|
||||
go install github.com/marte-community/marte-dev-tools/cmd/mdt@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI Commands
|
||||
|
||||
- **Init**: Initialize a MARTe project.
|
||||
```bash
|
||||
mdt init project_name
|
||||
```
|
||||
- **Check**: Run validation on a file or project.
|
||||
```bash
|
||||
mdt check path/to/project
|
||||
```
|
||||
- **Build**: Merge project files into a single output.
|
||||
```bash
|
||||
mdt build [-o output.marte] main.marte ...
|
||||
```
|
||||
- **Format**: Format configuration files.
|
||||
```bash
|
||||
mdt fmt path/to/file.marte
|
||||
```
|
||||
- **LSP**: Start the language server (used by editor plugins).
|
||||
```bash
|
||||
mdt lsp
|
||||
```
|
||||
|
||||
### Editor Integration
|
||||
|
||||
`mdt lsp` implements the Language Server Protocol. You can use it with any LSP-compatible editor (VS Code, Neovim, Emacs, etc.).
|
||||
|
||||
## MARTe Configuration
|
||||
|
||||
The tools support the MARTe configuration format with extended features:
|
||||
|
||||
- **Objects**: `+Node = { Class = ... }`
|
||||
- **Signals**: `Signal = { Type = ... }`
|
||||
- **Namespaces**: `#package PROJECT.NODE` for organizing multi-file projects.
|
||||
|
||||
### Validation & Schema
|
||||
|
||||
Validation is fully schema-driven using CUE.
|
||||
|
||||
- **Built-in Schema**: Covers standard MARTe classes (`StateMachine`, `GAM`, `DataSource`, `RealTimeApplication`, etc.).
|
||||
- **Custom Schema**: Add a `.marte_schema.cue` file to your project root to extend or override definitions.
|
||||
|
||||
**Example `.marte_schema.cue`:**
|
||||
|
||||
```cue
|
||||
package schema
|
||||
|
||||
#Classes: {
|
||||
MyCustomGAM: {
|
||||
#meta: {
|
||||
direction: "INOUT"
|
||||
multithreaded: true
|
||||
}
|
||||
Param1: int
|
||||
Param2?: string
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pragmas (Suppressing Warnings)
|
||||
|
||||
Use comments starting with `//!` to control validation behavior:
|
||||
|
||||
- `//! unused: Reason` - Suppress "Unused GAM" or "Unused Signal" warnings.
|
||||
- `//! implicit: Reason` - Suppress "Implicitly Defined Signal" warnings.
|
||||
- `//! cast(DefinedType, UsageType)` - Allow type mismatch between definition and usage (e.g. `//!cast(uint32, int32)`).
|
||||
- `//! allow(unused)` - Global suppression for the file.
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
go build ./cmd/mdt
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
192
cmd/mdt/main.go
192
cmd/mdt/main.go
@@ -3,20 +3,22 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/builder"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/formatter"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/logger"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/lsp"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
"github.com/marte-community/marte-dev-tools/internal/formatter"
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/logger"
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/validator"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
logger.Println("Usage: mdt <command> [arguments]")
|
||||
logger.Println("Commands: lsp, build, check, fmt")
|
||||
logger.Println("Commands: lsp, build, check, fmt, init")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -30,6 +32,8 @@ func main() {
|
||||
runCheck(os.Args[2:])
|
||||
case "fmt":
|
||||
runFmt(os.Args[2:])
|
||||
case "init":
|
||||
runInit(os.Args[2:])
|
||||
default:
|
||||
logger.Printf("Unknown command: %s\n", command)
|
||||
os.Exit(1)
|
||||
@@ -41,13 +45,86 @@ func runLSP() {
|
||||
}
|
||||
|
||||
func runBuild(args []string) {
|
||||
if len(args) < 1 {
|
||||
logger.Println("Usage: mdt build <input_files...>")
|
||||
files := []string{}
|
||||
overrides := make(map[string]string)
|
||||
outputFile := ""
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
if strings.HasPrefix(arg, "-v") {
|
||||
pair := arg[2:]
|
||||
parts := strings.SplitN(pair, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
overrides[parts[0]] = parts[1]
|
||||
}
|
||||
} else if arg == "-o" {
|
||||
if i+1 < len(args) {
|
||||
outputFile = args[i+1]
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
files = append(files, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(files) < 1 {
|
||||
logger.Println("Usage: mdt build [-o output] [-vVAR=VAL] <input_files...>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
b := builder.NewBuilder(args)
|
||||
err := b.Build(os.Stdout)
|
||||
// 1. Run Validation
|
||||
tree := index.NewProjectTree()
|
||||
for _, file := range files {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
logger.Printf("Error reading %s: %v\n", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
p := parser.NewParser(string(content))
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
logger.Printf("%s: Grammar error: %v\n", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tree.AddFile(file, config)
|
||||
}
|
||||
|
||||
v := validator.NewValidator(tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
hasErrors := false
|
||||
for _, diag := range v.Diagnostics {
|
||||
level := "ERROR"
|
||||
if diag.Level == validator.LevelWarning {
|
||||
level = "WARNING"
|
||||
} else {
|
||||
hasErrors = true
|
||||
}
|
||||
logger.Printf("%s:%d:%d: %s: %s\n", diag.File, diag.Position.Line, diag.Position.Column, level, diag.Message)
|
||||
}
|
||||
|
||||
if hasErrors {
|
||||
logger.Println("Build failed due to validation errors.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 2. Perform Build
|
||||
b := builder.NewBuilder(files, overrides)
|
||||
|
||||
var out *os.File = os.Stdout
|
||||
if outputFile != "" {
|
||||
f, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
logger.Printf("Error creating output file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
out = f
|
||||
}
|
||||
|
||||
err := b.Build(out)
|
||||
if err != nil {
|
||||
logger.Printf("Build failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -61,7 +138,7 @@ func runCheck(args []string) {
|
||||
}
|
||||
|
||||
tree := index.NewProjectTree()
|
||||
// configs := make(map[string]*parser.Configuration) // We don't strictly need this map if we just build the tree
|
||||
syntaxErrors := 0
|
||||
|
||||
for _, file := range args {
|
||||
content, err := os.ReadFile(file)
|
||||
@@ -71,23 +148,22 @@ func runCheck(args []string) {
|
||||
}
|
||||
|
||||
p := parser.NewParser(string(content))
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
logger.Printf("%s: Grammar error: %v\n", file, err)
|
||||
continue
|
||||
config, _ := p.Parse()
|
||||
if len(p.Errors()) > 0 {
|
||||
syntaxErrors += len(p.Errors())
|
||||
for _, e := range p.Errors() {
|
||||
logger.Printf("%s: Grammar error: %v\n", file, e)
|
||||
}
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
tree.AddFile(file, config)
|
||||
}
|
||||
}
|
||||
|
||||
// idx.ResolveReferences() // Not implemented in new tree yet, but Validator uses Tree directly
|
||||
v := validator.NewValidator(tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
// Legacy loop removed as ValidateProject covers it via recursion
|
||||
|
||||
v.CheckUnused()
|
||||
|
||||
for _, diag := range v.Diagnostics {
|
||||
level := "ERROR"
|
||||
if diag.Level == validator.LevelWarning {
|
||||
@@ -96,8 +172,9 @@ func runCheck(args []string) {
|
||||
logger.Printf("%s:%d:%d: %s: %s\n", diag.File, diag.Position.Line, diag.Position.Column, level, diag.Message)
|
||||
}
|
||||
|
||||
if len(v.Diagnostics) > 0 {
|
||||
logger.Printf("\nFound %d issues.\n", len(v.Diagnostics))
|
||||
totalIssues := len(v.Diagnostics) + syntaxErrors
|
||||
if totalIssues > 0 {
|
||||
logger.Printf("\nFound %d issues.\n", totalIssues)
|
||||
} else {
|
||||
logger.Println("No issues found.")
|
||||
}
|
||||
@@ -134,3 +211,70 @@ func runFmt(args []string) {
|
||||
logger.Printf("Formatted %s\n", file)
|
||||
}
|
||||
}
|
||||
|
||||
func runInit(args []string) {
|
||||
if len(args) < 1 {
|
||||
logger.Println("Usage: mdt init <project_name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projectName := args[0]
|
||||
if err := os.MkdirAll(filepath.Join(projectName, "src"), 0755); err != nil {
|
||||
logger.Fatalf("Error creating project directories: %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"Makefile": `MDT=mdt
|
||||
|
||||
all: check build
|
||||
|
||||
check:
|
||||
$(MDT) check src/*.marte
|
||||
|
||||
build:
|
||||
$(MDT) build -o app.marte src/*.marte
|
||||
|
||||
fmt:
|
||||
$(MDT) fmt src/*.marte
|
||||
`,
|
||||
".marte_schema.cue": `package schema
|
||||
|
||||
#Classes: {
|
||||
// Add your project-specific classes here
|
||||
}
|
||||
`,
|
||||
"src/app.marte": `#package App
|
||||
|
||||
+Main = {
|
||||
Class = RealTimeApplication
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+Run = {
|
||||
Class = RealTimeState
|
||||
+MainThread = {
|
||||
Class = RealTimeThread
|
||||
Functions = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
}
|
||||
}
|
||||
`,
|
||||
"src/components.marte": `#package App.Data
|
||||
|
||||
// Define your DataSources here
|
||||
`,
|
||||
}
|
||||
|
||||
for path, content := range files {
|
||||
fullPath := filepath.Join(projectName, path)
|
||||
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
|
||||
logger.Fatalf("Error creating file %s: %v", fullPath, err)
|
||||
}
|
||||
logger.Printf("Created %s\n", fullPath)
|
||||
}
|
||||
|
||||
logger.Printf("Project '%s' initialized successfully.\n", projectName)
|
||||
}
|
||||
121
docs/CODE_DOCUMENTATION.md
Normal file
121
docs/CODE_DOCUMENTATION.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# mdt Internal Code Documentation
|
||||
|
||||
This document provides a detailed overview of the `mdt` codebase architecture and internal components.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
`mdt` is built as a modular system where core functionalities are separated into internal packages. The data flow typically follows this pattern:
|
||||
|
||||
1. **Parsing**: Source code is parsed into an Abstract Syntax Tree (AST).
|
||||
2. **Indexing**: ASTs from multiple files are aggregated into a unified `ProjectTree`.
|
||||
3. **Processing**: The `ProjectTree` is used by the Validator, Builder, and LSP server to perform their respective tasks.
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
cmd/
|
||||
mdt/ # Application entry point (CLI)
|
||||
internal/
|
||||
builder/ # Logic for merging and building configurations
|
||||
formatter/ # Code formatting engine
|
||||
index/ # Symbol table and project structure management
|
||||
logger/ # Centralized logging
|
||||
lsp/ # Language Server Protocol implementation
|
||||
parser/ # Lexer, Parser, and AST definitions
|
||||
schema/ # CUE schema loading and integration
|
||||
validator/ # Semantic analysis and validation logic
|
||||
```
|
||||
|
||||
## Core Packages
|
||||
|
||||
### 1. `internal/parser`
|
||||
|
||||
Responsible for converting MARTe configuration text into structured data.
|
||||
|
||||
* **Lexer (`lexer.go`)**: Tokenizes the input stream. Handles MARTe specific syntax like `#package`, `#let`, `//!` pragmas, and `//#` docstrings. Supports standard identifiers and `#`-prefixed identifiers. Recognizes advanced number formats (hex `0x`, binary `0b`).
|
||||
* **Parser (`parser.go`)**: Recursive descent parser. Converts tokens into a `Configuration` object containing definitions, comments, and pragmas. Implements expression parsing with precedence.
|
||||
* **AST (`ast.go`)**: Defines the node types (`ObjectNode`, `Field`, `Value`, `VariableDefinition`, `BinaryExpression`, etc.). All nodes implement the `Node` interface providing position information.
|
||||
|
||||
### 2. `internal/index`
|
||||
|
||||
The brain of the system. It maintains a holistic view of the project.
|
||||
|
||||
* **ProjectTree**: The central data structure. It holds the root of the configuration hierarchy (`Root`), references, and isolated files.
|
||||
* **ScanDirectory**: Recursively walks the project directory to find all `.marte` files, adding them to the tree even if they contain partial syntax errors.
|
||||
* **ProjectNode**: Represents a logical node in the configuration. Since a node can be defined across multiple files (fragments), `ProjectNode` aggregates these fragments. It also stores locally defined variables and constants in its `Variables` map.
|
||||
* **NodeMap**: A hash map index (`map[string][]*ProjectNode`) for $O(1)$ symbol lookups, optimizing `FindNode` operations.
|
||||
* **Reference Resolution**: The `ResolveReferences` method links `Reference` objects to their target `ProjectNode` or `VariableDefinition`. It uses `ResolveName` (exported) which respects lexical scoping rules by searching the hierarchy upwards from the reference's container, using `FindNode` for deep searches within each scope.
|
||||
|
||||
### 3. `internal/validator`
|
||||
|
||||
Ensures configuration correctness.
|
||||
|
||||
* **Validator**: Iterates over the `ProjectTree` to check rules.
|
||||
* **Checks**:
|
||||
* **Structure**: Duplicate fields, invalid content.
|
||||
* **Schema**: Unifies nodes with CUE schemas (loaded via `internal/schema`) to validate types and mandatory fields.
|
||||
* **Signals**: Verifies that signals referenced in GAMs exist in DataSources and match types. Performs project-wide consistency checks for implicit signals.
|
||||
* **Threading**: Checks `CheckDataSourceThreading` to ensure non-multithreaded DataSources are not shared across threads in the same state.
|
||||
* **Ordering**: `CheckINOUTOrdering` verifies that for `INOUT` signals, the producing GAM appears before the consuming GAM in the thread's execution list.
|
||||
* **Variables**: `CheckVariables` validates variable values against their defined CUE types. Prevents external overrides of `#let` constants. `CheckUnresolvedVariables` ensures all used variables are defined.
|
||||
* **Unused**: Detects unused GAMs and Signals (suppressible via pragmas).
|
||||
|
||||
### 4. `internal/lsp`
|
||||
|
||||
Implements the Language Server Protocol.
|
||||
|
||||
* **Server (`server.go`)**: Handles JSON-RPC messages over stdio.
|
||||
* **Evaluation**: Implements a lightweight expression evaluator to show evaluated values in Hover and completion snippets.
|
||||
* **Incremental Sync**: Supports `textDocumentSync: 2`. `HandleDidChange` applies patches to the in-memory document buffers using `offsetAt` logic.
|
||||
* **Features**:
|
||||
* `HandleCompletion`: Context-aware suggestions (Macros, Schema fields, Signal references, Class names).
|
||||
* `HandleHover`: Shows documentation (including docstrings for variables), evaluated signal types/dimensions, and usage analysis.
|
||||
* `HandleDefinition` / `HandleReferences`: specific lookup using the `index`.
|
||||
* `HandleRename`: Project-wide renaming supporting objects, fields, and signals (including implicit ones).
|
||||
|
||||
### 5. `internal/builder`
|
||||
|
||||
Merges multiple MARTe files into a single output.
|
||||
|
||||
* **Logic**: It parses all input files, builds a temporary `ProjectTree`, and then reconstructs the source code.
|
||||
* **Merging**: It interleaves fields and subnodes from different file fragments to produce a coherent single-file configuration, respecting the `#package` hierarchy.
|
||||
* **Evaluation**: Evaluates all expressions and variable references into concrete MARTe values in the final output. Prevents overrides of `#let` constants.
|
||||
|
||||
### 6. `internal/schema`
|
||||
|
||||
Manages CUE schemas.
|
||||
|
||||
* **Loading**: Loads the embedded default schema (`marte.cue`) and merges it with any user-provided `.marte_schema.cue`.
|
||||
* **Metadata**: Handles the `#meta` field in schemas to extract properties like `direction` and `multithreaded` support for the validator.
|
||||
|
||||
## Key Data Flows
|
||||
|
||||
### Reference Resolution
|
||||
1. **Scan**: Files are parsed and added to the `ProjectTree`.
|
||||
2. **Index**: `RebuildIndex` populates `NodeMap`.
|
||||
3. **Resolve**: `ResolveReferences` iterates all recorded references (values) and calls `FindNode`.
|
||||
4. **Link**: If found, `ref.Target` is set to the `ProjectNode`.
|
||||
|
||||
### Validation Lifecycle
|
||||
1. `mdt check` or LSP `didChange` triggers validation.
|
||||
2. A new `Validator` is created with the current `Tree`.
|
||||
3. `ValidateProject` is called.
|
||||
4. It walks the tree, runs checks, and populates `Diagnostics`.
|
||||
5. Diagnostics are printed (CLI) or published via `textDocument/publishDiagnostics` (LSP).
|
||||
|
||||
### Threading Check Logic
|
||||
1. Iterates all `RealTimeApplication` nodes found in the project.
|
||||
2. For each App:
|
||||
1. Finds `States` and `Threads`.
|
||||
2. For each Thread, resolves the `Functions` (GAMs).
|
||||
3. For each GAM, resolves connected `DataSources` via Input/Output signals.
|
||||
4. Maps `DataSource -> Thread` within the context of a State.
|
||||
5. If a DataSource is seen in >1 Thread, it checks the `#meta.multithreaded` property. If false (default), an error is raised.
|
||||
|
||||
### INOUT Ordering Logic
|
||||
1. Iterates Threads.
|
||||
2. Iterates GAMs in execution order.
|
||||
3. Tracks `producedSignals` and `consumedSignals`.
|
||||
4. For each GAM, checks Inputs. If Input is `INOUT` (and not multithreaded) and not in `producedSignals`, reports "Consumed before Produced" error.
|
||||
5. Registers Outputs in `producedSignals`.
|
||||
6. At end of thread, checks for signals that were produced but never consumed, reporting a warning.
|
||||
255
docs/CONFIGURATION_GUIDE.md
Normal file
255
docs/CONFIGURATION_GUIDE.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# MARTe Configuration Guide
|
||||
|
||||
This guide explains the syntax, features, and best practices for writing MARTe configurations using `mdt`.
|
||||
|
||||
## 1. Syntax Overview
|
||||
|
||||
MARTe configurations use a hierarchical object-oriented syntax.
|
||||
|
||||
### Objects (Nodes)
|
||||
Objects are defined using `+` (public/instantiated) or `$` (template/class-like) prefixes. Every object **must** have a `Class` field.
|
||||
|
||||
```marte
|
||||
+MyObject = {
|
||||
Class = MyClass
|
||||
Field1 = 100
|
||||
Field2 = "Hello"
|
||||
}
|
||||
```
|
||||
|
||||
### Fields and Values
|
||||
- **Fields**: Alphanumeric identifiers (e.g., `Timeout`, `CycleTime`).
|
||||
- **Values**:
|
||||
- Integers: `10`, `-5`, `0xFA`, `0b1011`
|
||||
- Floats: `3.14`, `1e-3`
|
||||
- Strings: `"Text"`
|
||||
- Booleans: `true`, `false`
|
||||
- References: `MyObject`, `MyObject.SubNode`
|
||||
- Arrays: `{ 1 2 3 }` or `{ "A" "B" }`
|
||||
|
||||
## 2. Signals and Data Flow
|
||||
|
||||
Signals define how data moves between DataSources (drivers) and GAMs (algorithms).
|
||||
|
||||
### Defining Signals
|
||||
Signals are typically defined in a `DataSource`. They must have a `Type`.
|
||||
|
||||
```marte
|
||||
+MyDataSource = {
|
||||
Class = GAMDataSource
|
||||
Signals = {
|
||||
Signal1 = { Type = uint32 }
|
||||
Signal2 = { Type = float32 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Signals in GAMs
|
||||
GAMs declare inputs and outputs. You can refer to signals directly or alias them.
|
||||
|
||||
```marte
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Signal1 = {
|
||||
DataSource = MyDataSource
|
||||
Type = uint32 // Must match DataSource definition
|
||||
}
|
||||
MyAlias = {
|
||||
Alias = Signal2
|
||||
DataSource = MyDataSource
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Multi-file Projects
|
||||
|
||||
You can split your configuration into multiple files.
|
||||
|
||||
### Namespaces
|
||||
Use `#package` to define where the file's content fits in the hierarchy.
|
||||
|
||||
**file1.marte**
|
||||
```marte
|
||||
#package MyApp.Controller
|
||||
+MyController = { ... }
|
||||
```
|
||||
|
||||
This places `MyController` under `MyApp.Controller`.
|
||||
|
||||
### Building
|
||||
The `build` command merges all files.
|
||||
|
||||
```bash
|
||||
mdt build -o final.marte src/*.marte
|
||||
```
|
||||
|
||||
## 4. Variables and Constants
|
||||
|
||||
You can define variables to parameterize your configuration.
|
||||
|
||||
### Variables (`#var`)
|
||||
Variables can be defined at any level and can be overridden externally (e.g., via CLI).
|
||||
|
||||
```marte
|
||||
//# Default timeout
|
||||
#var Timeout: uint32 = 100
|
||||
|
||||
+MyObject = {
|
||||
Class = Timer
|
||||
Timeout = $Timeout
|
||||
}
|
||||
```
|
||||
|
||||
### Constants (`#let`)
|
||||
Constants are like variables but **cannot** be overridden externally. They are ideal for internal calculations or fixed parameters.
|
||||
|
||||
```marte
|
||||
//# Sampling period
|
||||
#let Ts: float64 = 0.001
|
||||
|
||||
+Clock = {
|
||||
Class = HighResClock
|
||||
Period = @Ts
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Syntax
|
||||
Reference a variable or constant using `$` or `@`:
|
||||
|
||||
```marte
|
||||
Field = $MyVar
|
||||
// or
|
||||
Field = @MyVar
|
||||
```
|
||||
|
||||
### Expressions
|
||||
You can use operators in field values. Supported operators:
|
||||
- **Math**: `+`, `-`, `*`, `/`, `%`, `^` (XOR), `&`, `|` (Bitwise)
|
||||
- **String Concatenation**: `..`
|
||||
- **Parentheses**: `(...)` for grouping
|
||||
|
||||
```marte
|
||||
Field1 = 10 + 20 * 2 // 50
|
||||
Field2 = "Hello " .. "World"
|
||||
Field3 = ($MyVar + 5) * 2
|
||||
```
|
||||
|
||||
### Build Override
|
||||
You can override variable values during build (only for `#var`):
|
||||
|
||||
```bash
|
||||
mdt build -vMyVar=200 src/*.marte
|
||||
```
|
||||
|
||||
## 5. Comments and Documentation
|
||||
|
||||
- Line comments: `// This is a comment`
|
||||
- Docstrings: `//# This documents the following node`. These appear in hover tooltips.
|
||||
|
||||
```marte
|
||||
//# This is the main application
|
||||
+App = { ... }
|
||||
```
|
||||
|
||||
Docstrings work for objects, fields, variables, and constants.
|
||||
|
||||
## 6. Schemas and Validation
|
||||
|
||||
`mdt` validates your configuration against CUE schemas.
|
||||
|
||||
### Built-in Schema
|
||||
Common classes (`RealTimeApplication`, `StateMachine`, `IOGAM`, etc.) are built-in.
|
||||
|
||||
### Custom Schemas
|
||||
You can extend the schema by creating a `.marte_schema.cue` file in your project root.
|
||||
|
||||
**Example: Adding a custom GAM**
|
||||
|
||||
```cue
|
||||
package schema
|
||||
|
||||
#Classes: {
|
||||
MyCustomGAM: {
|
||||
// Metadata for Validator/LSP
|
||||
#meta: {
|
||||
direction: "INOUT" // "IN", "OUT", "INOUT"
|
||||
multithreaded: false
|
||||
}
|
||||
|
||||
// Fields
|
||||
Gain: float
|
||||
Offset?: float // Optional
|
||||
InputSignals: {...}
|
||||
OutputSignals: {...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Pragmas (Suppressing Warnings)
|
||||
|
||||
If validation is too strict, you can suppress warnings using pragmas (`//!`).
|
||||
|
||||
- **Suppress Unused Warning**:
|
||||
```marte
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
//! ignore(unused): This GAM is triggered externally
|
||||
}
|
||||
```
|
||||
|
||||
- **Suppress Implicit Signal Warning**:
|
||||
```marte
|
||||
InputSignals = {
|
||||
//! ignore(implicit)
|
||||
ImplicitSig = { Type = uint32 }
|
||||
}
|
||||
```
|
||||
|
||||
- **Type Casting**:
|
||||
```marte
|
||||
Sig1 = {
|
||||
//! cast(uint32, int32): Intentional mismatch
|
||||
DataSource = DS
|
||||
Type = int32
|
||||
}
|
||||
```
|
||||
|
||||
- **Global Suppression**:
|
||||
```marte
|
||||
//! allow(unused)
|
||||
//! allow(implicit)
|
||||
```
|
||||
|
||||
## 8. Validation Rules (Detail)
|
||||
|
||||
### Data Flow Validation
|
||||
`mdt` checks for logical data flow errors:
|
||||
- **Consumed before Produced**: If a GAM reads an INOUT signal that hasn't been written by a previous GAM in the same cycle, an error is reported.
|
||||
- **Produced but not Consumed**: If a GAM writes an INOUT signal that is never read by subsequent GAMs, a warning is reported.
|
||||
- **Initialization**: Providing a `Value` field in an `InputSignal` treats it as "produced" (initialized), resolving "Consumed before Produced" errors.
|
||||
|
||||
### Threading Rules
|
||||
A DataSource that is **not** marked as multithreaded (default) cannot be used by GAMs running in different threads within the same State.
|
||||
|
||||
To allow sharing, the DataSource class in the schema must have `#meta: multithreaded: true`.
|
||||
|
||||
### Implicit vs Explicit Signals
|
||||
- **Explicit**: Signal defined in `DataSource.Signals`.
|
||||
- **Implicit**: Signal used in GAM but not defined in DataSource. `mdt` reports a warning unless suppressed.
|
||||
- **Consistency**: All references to the same logical signal (same name in same DataSource) must share the same `Type` and size properties.
|
||||
|
||||
## 9. Editor Features (LSP)
|
||||
|
||||
The `mdt` LSP server provides several features to improve productivity.
|
||||
|
||||
### Inlay Hints
|
||||
Inlay hints provide real-time contextual information directly in the editor:
|
||||
|
||||
- **Signal Metadata**: Signal usages in GAMs display their evaluated type and size, e.g., `Sig1` **`::uint32[10x1]`**.
|
||||
- **Object Class**: References to objects show the object's class, e.g., `DataSource = ` **`FileReader::`** `DS`.
|
||||
- **Expression Evaluation**:
|
||||
- Complex expressions show their result at the end of the line, e.g., `Expr = 10 + 20` **` => 30`**.
|
||||
- Variable references show their current value inline, e.g., `@MyVar` **`(=> 10)`**.
|
||||
159
docs/EDITOR_INTEGRATION.md
Normal file
159
docs/EDITOR_INTEGRATION.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Editor Integration Guide
|
||||
|
||||
`mdt` includes a Language Server Protocol (LSP) implementation that provides features like:
|
||||
|
||||
- Syntax highlighting and error reporting (Parser & Semantic)
|
||||
- Auto-completion
|
||||
- Go to Definition / References
|
||||
- Hover documentation
|
||||
- Symbol renaming
|
||||
- Incremental synchronization (Robust)
|
||||
|
||||
The LSP server is started via the command:
|
||||
|
||||
```bash
|
||||
mdt lsp
|
||||
```
|
||||
|
||||
It communicates via **stdio**.
|
||||
|
||||
## VS Code
|
||||
|
||||
You can use a generic LSP extension like [Generic LSP Client](https://marketplace.visualstudio.com/items?itemName=summne.vscode-generic-lsp-client) or configure a custom task.
|
||||
|
||||
**Using "Run on Save" or similar extensions is an option, but for true LSP support:**
|
||||
|
||||
1. Install the **"glspc"** (Generic LSP Client) extension or similar.
|
||||
2. Configure it in your `settings.json`:
|
||||
|
||||
```json
|
||||
"glspc.languageServer configurations": [
|
||||
{
|
||||
"languageId": "marte",
|
||||
"command": "mdt",
|
||||
"args": ["lsp"],
|
||||
"rootUri": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
3. Associate `.marte` files with the language ID:
|
||||
|
||||
```json
|
||||
"files.associations": {
|
||||
"*.marte": "marte"
|
||||
}
|
||||
```
|
||||
|
||||
## Neovim (Native LSP)
|
||||
|
||||
Add the following to your `init.lua` or `init.vim` (using `nvim-lspconfig`):
|
||||
|
||||
```lua
|
||||
local lspconfig = require'lspconfig'
|
||||
local configs = require'lspconfig.configs'
|
||||
|
||||
if not configs.marte then
|
||||
configs.marte = {
|
||||
default_config = {
|
||||
cmd = {'mdt', 'lsp'},
|
||||
filetypes = {'marte'},
|
||||
root_dir = lspconfig.util.root_pattern('.git', 'go.mod', '.marte_schema.cue'),
|
||||
settings = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
lspconfig.marte.setup{}
|
||||
|
||||
-- Add filetype detection
|
||||
vim.cmd([[
|
||||
autocmd BufNewFile,BufRead *.marte setfiletype marte
|
||||
]])
|
||||
```
|
||||
|
||||
## Helix
|
||||
|
||||
Add this to your `languages.toml` (usually in `~/.config/helix/languages.toml`):
|
||||
|
||||
```toml
|
||||
[[language]]
|
||||
name = "marte"
|
||||
scope = "source.marte"
|
||||
injection-regex = "marte"
|
||||
file-types = ["marte"]
|
||||
roots = [".git", ".marte_schema.cue"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = [ "mdt-lsp" ]
|
||||
|
||||
[language-server.mdt-lsp]
|
||||
command = "mdt"
|
||||
args = ["lsp"]
|
||||
```
|
||||
|
||||
## Vim
|
||||
|
||||
### Using `vim-lsp`
|
||||
|
||||
```vim
|
||||
if executable('mdt')
|
||||
au User lsp_setup call lsp#register_server({
|
||||
\ 'name': 'mdt-lsp',
|
||||
\ 'cmd': {server_info->['mdt', 'lsp']},
|
||||
\ 'whitelist': ['marte'],
|
||||
\ })
|
||||
endif
|
||||
|
||||
au BufRead,BufNewFile *.marte set filetype=marte
|
||||
```
|
||||
|
||||
### Using `ALE`
|
||||
|
||||
```vim
|
||||
call ale#linter#define('marte', {
|
||||
\ 'name': 'mdt',
|
||||
\ 'lsp': 'stdio',
|
||||
\ 'executable': 'mdt',
|
||||
\ 'command': '%e lsp',
|
||||
\ 'project_root': function('ale#handlers#python#FindProjectRoot'),
|
||||
\})
|
||||
```
|
||||
|
||||
## Zed
|
||||
|
||||
Add to your `settings.json`:
|
||||
|
||||
```json
|
||||
"lsp": {
|
||||
"marte": {
|
||||
"binary": {
|
||||
"path": "mdt",
|
||||
"arguments": ["lsp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kakoune (kak-lsp)
|
||||
|
||||
In your `kak-lsp.toml`:
|
||||
|
||||
```toml
|
||||
[language.marte]
|
||||
filetypes = ["marte"]
|
||||
roots = [".git", ".marte_schema.cue"]
|
||||
command = "mdt"
|
||||
args = ["lsp"]
|
||||
```
|
||||
|
||||
## Eclipse
|
||||
|
||||
1. Install **LSP4E** plugin.
|
||||
2. Go to **Preferences > Language Servers**.
|
||||
3. Add a new Language Server:
|
||||
- **Content Type**: Text / Custom (Associate `*.marte` with a content type).
|
||||
- **Launch configuration**: Program.
|
||||
- **Command**: `mdt`
|
||||
- **Arguments**: `lsp`
|
||||
- **Input/Output**: Standard Input/Output.
|
||||
212
docs/TUTORIAL.md
Normal file
212
docs/TUTORIAL.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Creating a MARTe Application with mdt
|
||||
|
||||
This tutorial will guide you through creating, building, and validating a complete MARTe application using the `mdt` toolset.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `mdt` installed and available in your PATH.
|
||||
- `make` (optional but recommended).
|
||||
|
||||
## Step 1: Initialize the Project
|
||||
|
||||
Start by creating a new project named `MyControlApp`.
|
||||
|
||||
```bash
|
||||
mdt init MyControlApp
|
||||
cd MyControlApp
|
||||
```
|
||||
|
||||
This command creates a standard project structure:
|
||||
|
||||
- `Makefile`: For building and checking the project.
|
||||
- `.marte_schema.cue`: For defining custom schemas (if needed).
|
||||
- `src/app.marte`: The main application definition.
|
||||
- `src/components.marte`: A placeholder for defining components (DataSources).
|
||||
|
||||
## Step 2: Define Components
|
||||
|
||||
Open `src/components.marte`. This file uses the `#package App.Data` namespace, meaning all definitions here will be children of `App.Data`.
|
||||
|
||||
Let's define a **Timer** (input source) and a **Logger** (output destination).
|
||||
|
||||
```marte
|
||||
#package MyContollApp.App.Data
|
||||
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+TimingDataSource = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
+Timer = {
|
||||
Class = LinuxTimer
|
||||
Signals = {
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+Logger = {
|
||||
Class = LoggerDataSource
|
||||
Signals = {
|
||||
LogValue = {
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Implement Logic (GAM)
|
||||
|
||||
Open `src/app.marte`. This file defines the `App` node.
|
||||
|
||||
We will add a GAM that takes the time from the Timer, converts it, and logs it.
|
||||
|
||||
Add the GAM definition inside the `+Main` object (or as a separate object if you prefer modularity). Let's modify `src/app.marte`:
|
||||
|
||||
```marte
|
||||
#package MyContollApp
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+Functions = {
|
||||
Class = RefenceContainer
|
||||
// Define the GAM
|
||||
+Converter = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
TimeIn = {
|
||||
DataSource = Timer
|
||||
Type = uint32
|
||||
Frequency = 100 //Hz
|
||||
Alias = Time // Refers to 'Time' signal in Timer
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
LogOut = {
|
||||
DataSource = Logger
|
||||
Type = float32
|
||||
Alias = LogValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+Run = {
|
||||
Class = RealTimeState
|
||||
+MainThread = {
|
||||
Class = RealTimeThread
|
||||
Functions = { Converter } // Run our GAM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = TimingDataSource
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Validate
|
||||
|
||||
Run the validation check to ensure everything is correct (types match, references are valid).
|
||||
|
||||
```bash
|
||||
mdt check src/*.marte
|
||||
```
|
||||
|
||||
Or using Make:
|
||||
|
||||
```bash
|
||||
make check
|
||||
```
|
||||
|
||||
If you made a mistake (e.g., mismatched types), `mdt` will report an error.
|
||||
|
||||
## Step 5: Build
|
||||
|
||||
Merge all files into a single configuration file.
|
||||
|
||||
```bash
|
||||
mdt build -o final_app.marte src/*.marte
|
||||
```
|
||||
|
||||
Or using Make:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
This produces `app.marte` (or `final_app.marte`), which contains the flattened, merged configuration ready for the MARTe framework.
|
||||
|
||||
## Step 6: Using Variables and Expressions
|
||||
|
||||
You can parameterize your application using variables. Let's define a constant for the sampling frequency.
|
||||
|
||||
Modify `src/app.marte`:
|
||||
|
||||
```marte
|
||||
#package MyContollApp
|
||||
|
||||
//# Sampling frequency in Hz
|
||||
#let SamplingFreq: uint32 = 100
|
||||
|
||||
+App = {
|
||||
// ...
|
||||
+Functions = {
|
||||
+Converter = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
TimeIn = {
|
||||
DataSource = Timer
|
||||
Type = uint32
|
||||
Frequency = $SamplingFreq
|
||||
Alias = Time
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also use expressions for calculations:
|
||||
|
||||
```marte
|
||||
#let CycleTime: float64 = 1.0 / $SamplingFreq
|
||||
```
|
||||
|
||||
LSP will show you the evaluated values directly in the code via **Inlay Hints** (e.g., `CycleTime: 0.01`) and in the hover documentation.
|
||||
|
||||
## Step 7: Advanced - Custom Schema
|
||||
|
||||
Suppose you want to enforce that your DataSources support multithreading. You can modify `.marte_schema.cue`.
|
||||
|
||||
```cue
|
||||
package schema
|
||||
|
||||
#Classes: {
|
||||
// Enforce that LinuxTimer must be multithreaded (example)
|
||||
LinuxTimer: {
|
||||
#meta: {
|
||||
multithreaded: true
|
||||
}
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, if you use `LinuxTimer` in multiple threads, `mdt check` will allow it (because of `#meta.multithreaded: true`). By default, it would disallow it.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You have successfully initialized, implemented, validated, and built a MARTe application using `mdt`.
|
||||
44
examples/README.md
Normal file
44
examples/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Examples
|
||||
|
||||
This directory contains example projects demonstrating different features and usage patterns of `mdt`.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
examples/
|
||||
simple/ # A basic, single-file application
|
||||
complex/ # A multi-file project with custom schema
|
||||
README.md # This file
|
||||
```
|
||||
|
||||
## Running Examples
|
||||
|
||||
Prerequisite: `mdt` must be built (or installed). The Makefiles in the examples assume `mdt` is available at `../../build/mdt`.
|
||||
|
||||
### Simple Project
|
||||
|
||||
Demonstrates a minimal setup:
|
||||
- Single `main.marte` file.
|
||||
- Basic Thread and GAM definition.
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
cd simple
|
||||
make check
|
||||
make build
|
||||
```
|
||||
|
||||
### Complex Project
|
||||
|
||||
Demonstrates advanced features:
|
||||
- **Multi-file Structure**: `src/app.marte` (Logic) and `src/components.marte` (Data).
|
||||
- **Namespaces**: Use of `#package` to organize nodes.
|
||||
- **Custom Schema**: `.marte_schema.cue` defines a custom class (`CustomController`) with specific metadata (`#meta.multithreaded`).
|
||||
- **Validation**: Enforces strict typing and custom rules.
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
cd complex
|
||||
make check
|
||||
make build
|
||||
```
|
||||
12
examples/complex/.marte_schema.cue
Normal file
12
examples/complex/.marte_schema.cue
Normal file
@@ -0,0 +1,12 @@
|
||||
package schema
|
||||
|
||||
#Classes: {
|
||||
CustomController: {
|
||||
#meta: {
|
||||
multithreaded: false
|
||||
}
|
||||
Gain: float
|
||||
InputSignals: {...}
|
||||
OutputSignals: {...}
|
||||
}
|
||||
}
|
||||
12
examples/complex/Makefile
Normal file
12
examples/complex/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
MDT=../../build/mdt
|
||||
|
||||
all: check build
|
||||
|
||||
check:
|
||||
$(MDT) check src/*.marte
|
||||
|
||||
build:
|
||||
$(MDT) build -o app_full.marte src/*.marte
|
||||
|
||||
fmt:
|
||||
$(MDT) fmt src/*.marte
|
||||
42
examples/complex/src/app.marte
Normal file
42
examples/complex/src/app.marte
Normal file
@@ -0,0 +1,42 @@
|
||||
#package complex_ex
|
||||
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+Run = {
|
||||
Class = RealTimeState
|
||||
+ControlThread = {
|
||||
Class = RealTimeThread
|
||||
Functions = { Controller }
|
||||
}
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+Controller = {
|
||||
Class = CustomController // Defined in .marte_schema.cue
|
||||
Gain = 10.5
|
||||
InputSignals = {
|
||||
Ref = {
|
||||
DataSource = App.Data.References
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Actuation = {
|
||||
DataSource = App.Data.Actuators
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB1
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = TimingDS
|
||||
}
|
||||
}
|
||||
24
examples/complex/src/components.marte
Normal file
24
examples/complex/src/components.marte
Normal file
@@ -0,0 +1,24 @@
|
||||
#package complex_ex.App.Data
|
||||
|
||||
+References = {
|
||||
Class = GAMDataSource
|
||||
Signals = {
|
||||
Ref = {
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
+Actuators = {
|
||||
Class = GAMDataSource
|
||||
Signals = {
|
||||
Actuation = {
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
+TimingDS = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
+DDB1 = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//!allow(unused): Ignore unused GAMs in this file
|
||||
//!allow(implicit): Ignore implicit signals in this file
|
||||
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
+MyDS = {
|
||||
Class = FileReader
|
||||
Filename = "test"
|
||||
Signals = {}
|
||||
}
|
||||
}
|
||||
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
// Implicit signal (not in MyDS)
|
||||
ImplicitSig = {
|
||||
DataSource = MyDS
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unused GAM
|
||||
+UnusedGAM = {
|
||||
Class = IOGAM
|
||||
}
|
||||
12
examples/simple/Makefile
Normal file
12
examples/simple/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
MDT=../../build/mdt
|
||||
|
||||
all: check build
|
||||
|
||||
check:
|
||||
$(MDT) check main.marte
|
||||
|
||||
build:
|
||||
$(MDT) build -o output.marte main.marte
|
||||
|
||||
fmt:
|
||||
$(MDT) fmt main.marte
|
||||
60
examples/simple/main.marte
Normal file
60
examples/simple/main.marte
Normal file
@@ -0,0 +1,60 @@
|
||||
//# Main Application
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB1
|
||||
+Timer = {
|
||||
Class = LinuxTimer
|
||||
Signals = {
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
//! unused: Time variable is not used
|
||||
Time = {
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
+Logger = {
|
||||
Class = LoggerDataSource
|
||||
}
|
||||
+DDB1 = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+Idle = {
|
||||
Class = RealTimeState
|
||||
+Thread1 = {
|
||||
Class = RealTimeThread
|
||||
CPUs = 0x1
|
||||
Functions = { MyGAM }
|
||||
}
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Counter = {
|
||||
DataSource = Timer
|
||||
Type = uint32
|
||||
Frequency = 100 //Hz
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
CounterCopy = {
|
||||
DataSource = Logger
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = Timer
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
19
go.mod
19
go.mod
@@ -1,3 +1,18 @@
|
||||
module github.com/marte-dev/marte-dev-tools
|
||||
module github.com/marte-community/marte-dev-tools
|
||||
|
||||
go 1.25.6
|
||||
go 1.25
|
||||
|
||||
require cuelang.org/go v0.15.3
|
||||
|
||||
require (
|
||||
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
|
||||
github.com/emicklei/proto v1.14.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
||||
53
go.sum
Normal file
53
go.sum
Normal file
@@ -0,0 +1,53 @@
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 h1:4k1yAtPvZJZQTu8DRY8muBo0LHv6TqtrE0AO5n6IPYs=
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084/go.mod h1:4WWeZNxUO1vRoZWAHIG0KZOd6dA25ypyWuwD3ti0Tdc=
|
||||
cuelang.org/go v0.15.3 h1:JKR/lZVwuIGlLTGIaJ0jONz9+CK3UDx06sQ6DDxNkaE=
|
||||
cuelang.org/go v0.15.3/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE=
|
||||
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
|
||||
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
|
||||
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
|
||||
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 h1:s1LvMaU6mVwoFtbxv/rCZKE7/fwDmDY684FfUe4c1Io=
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -6,16 +6,18 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
type Builder struct {
|
||||
Files []string
|
||||
Overrides map[string]string
|
||||
variables map[string]parser.Value
|
||||
}
|
||||
|
||||
func NewBuilder(files []string) *Builder {
|
||||
return &Builder{Files: files}
|
||||
func NewBuilder(files []string, overrides map[string]string) *Builder {
|
||||
return &Builder{Files: files, Overrides: overrides, variables: make(map[string]parser.Value)}
|
||||
}
|
||||
|
||||
func (b *Builder) Build(f *os.File) error {
|
||||
@@ -56,114 +58,95 @@ func (b *Builder) Build(f *os.File) error {
|
||||
tree.AddFile(file, config)
|
||||
}
|
||||
|
||||
b.collectVariables(tree)
|
||||
|
||||
if expectedProject == "" {
|
||||
for _, iso := range tree.IsolatedFiles {
|
||||
tree.Root.Fragments = append(tree.Root.Fragments, iso.Fragments...)
|
||||
for name, child := range iso.Children {
|
||||
if existing, ok := tree.Root.Children[name]; ok {
|
||||
b.mergeNodes(existing, child)
|
||||
} else {
|
||||
tree.Root.Children[name] = child
|
||||
child.Parent = tree.Root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
b.writeNodeContent(f, tree.Root, 0)
|
||||
b.writeNodeBody(f, rootNode, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent int) {
|
||||
indentStr := strings.Repeat(" ", indent)
|
||||
|
||||
// If this node has a RealName (e.g. +App), we print it as an object definition
|
||||
if node.RealName != "" {
|
||||
fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName)
|
||||
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])
|
||||
})
|
||||
|
||||
indentStr := strings.Repeat(" ", indent)
|
||||
|
||||
// If this node has a RealName (e.g. +App), we print it as an object definition
|
||||
// UNLESS it is the top-level output file itself?
|
||||
// If we are writing "App.marte", maybe we are writing the *body* of App?
|
||||
// Spec: "unifying multi-file project into a single configuration output"
|
||||
|
||||
// Let's assume we print the Node itself.
|
||||
if node.RealName != "" {
|
||||
fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName)
|
||||
indent++
|
||||
indentStr = strings.Repeat(" ", indent)
|
||||
}
|
||||
writtenChildren := make(map[string]bool)
|
||||
|
||||
// 2. Write definitions from fragments
|
||||
for _, frag := range node.Fragments {
|
||||
// Use formatter logic to print definitions
|
||||
// We need a temporary Config to use Formatter?
|
||||
// Or just reimplement basic printing? Formatter is better.
|
||||
// But Formatter prints to io.Writer.
|
||||
|
||||
// We can reuse formatDefinition logic if we exposed it, or just copy basic logic.
|
||||
// Since we need to respect indentation, using Formatter.Format might be tricky
|
||||
// unless we wrap definitions in a dummy structure.
|
||||
|
||||
for _, def := range frag.Definitions {
|
||||
// Basic formatting for now, referencing formatter style
|
||||
b.writeDefinition(f, def, indent)
|
||||
switch d := def.(type) {
|
||||
case *parser.Field:
|
||||
b.writeDefinition(f, d, indent)
|
||||
case *parser.VariableDefinition:
|
||||
continue
|
||||
case *parser.ObjectNode:
|
||||
norm := index.NormalizeName(d.Name)
|
||||
if child, ok := node.Children[norm]; ok {
|
||||
if !writtenChildren[norm] {
|
||||
b.writeNodeContent(f, child, indent)
|
||||
writtenChildren[norm] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Write Children (recursively)
|
||||
// Children are sub-nodes defined implicitly via #package A.B or explicitly +Sub
|
||||
// Explicit +Sub are handled via Fragments logic (they are definitions in fragments).
|
||||
// Implicit nodes (from #package A.B.C where B was never explicitly defined)
|
||||
// show up in Children map but maybe not in Fragments?
|
||||
|
||||
// If a Child is NOT in fragments (implicit), we still need to write it.
|
||||
// If it IS in fragments (explicit +Child), it was handled in loop above?
|
||||
// Wait. My Indexer puts `+Sub` into `node.Children["Sub"]` AND adds a `Fragment` to `node` containing `+Sub` object?
|
||||
|
||||
// Let's check Indexer.
|
||||
// Case ObjectNode:
|
||||
// Adds Fragment to `child` (the Sub node).
|
||||
// Does NOT add `ObjectNode` definition to `node`'s fragment list?
|
||||
// "pt.addObjectFragment(child...)"
|
||||
// It does NOT add to `fileFragment.Definitions`.
|
||||
|
||||
// So `node.Fragments` only contains Fields!
|
||||
// Children are all in `node.Children`.
|
||||
|
||||
// So:
|
||||
// 1. Write Fields (from Fragments).
|
||||
// 2. Write Children (from Children map).
|
||||
|
||||
// But wait, Fragments might have order?
|
||||
// "Relative ordering within a file is preserved."
|
||||
// My Indexer splits Fields and Objects.
|
||||
// Fields go to Fragments. Objects go to Children.
|
||||
// This loses the relative order between Fields and Objects in the source file!
|
||||
|
||||
// Correct Indexer approach for preserving order:
|
||||
// `Fragment` should contain a list of `Entry`.
|
||||
// `Entry` can be `Field` OR `ChildNodeName`.
|
||||
|
||||
// But I just rewrote Indexer to split them.
|
||||
// If strict order is required "within a file", my Indexer is slightly lossy regarding Field vs Object order.
|
||||
// Spec: "Relative ordering within a file is preserved."
|
||||
|
||||
// To fix this without another full rewrite:
|
||||
// Iterating `node.Children` alphabetically is arbitrary.
|
||||
// We should ideally iterate them in the order they appear.
|
||||
|
||||
// For now, I will proceed with writing Children after Fields, which is a common convention,
|
||||
// unless strict interleaving is required.
|
||||
// Given "Class first" rule, reordering happens anyway.
|
||||
|
||||
// Sorting Children?
|
||||
// Maybe keep a list of OrderedChildren in ProjectNode?
|
||||
|
||||
sortedChildren := make([]string, 0, len(node.Children))
|
||||
for k := range node.Children {
|
||||
if !writtenChildren[k] {
|
||||
sortedChildren = append(sortedChildren, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(sortedChildren) // Alphabetical for determinism
|
||||
|
||||
for _, k := range sortedChildren {
|
||||
child := node.Children[k]
|
||||
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) {
|
||||
@@ -175,6 +158,7 @@ func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int)
|
||||
}
|
||||
|
||||
func (b *Builder) formatValue(val parser.Value) string {
|
||||
val = b.evaluate(val)
|
||||
switch v := val.(type) {
|
||||
case *parser.StringValue:
|
||||
if v.Quoted {
|
||||
@@ -187,6 +171,8 @@ func (b *Builder) formatValue(val parser.Value) string {
|
||||
return v.Raw
|
||||
case *parser.BoolValue:
|
||||
return fmt.Sprintf("%v", v.Value)
|
||||
case *parser.VariableReferenceValue:
|
||||
return v.Name
|
||||
case *parser.ReferenceValue:
|
||||
return v.Value
|
||||
case *parser.ArrayValue:
|
||||
@@ -200,6 +186,18 @@ func (b *Builder) formatValue(val parser.Value) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) mergeNodes(dest, src *index.ProjectNode) {
|
||||
dest.Fragments = append(dest.Fragments, src.Fragments...)
|
||||
for name, child := range src.Children {
|
||||
if existing, ok := dest.Children[name]; ok {
|
||||
b.mergeNodes(existing, child)
|
||||
} else {
|
||||
dest.Children[name] = child
|
||||
child.Parent = dest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasClass(frag *index.Fragment) bool {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok && f.Name == "Class" {
|
||||
@@ -208,3 +206,139 @@ func hasClass(frag *index.Fragment) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Builder) collectVariables(tree *index.ProjectTree) {
|
||||
processNode := func(n *index.ProjectNode) {
|
||||
for _, frag := range n.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if vdef, ok := def.(*parser.VariableDefinition); ok {
|
||||
if valStr, ok := b.Overrides[vdef.Name]; ok {
|
||||
if !vdef.IsConst {
|
||||
p := parser.NewParser("Temp = " + valStr)
|
||||
cfg, _ := p.Parse()
|
||||
if len(cfg.Definitions) > 0 {
|
||||
if f, ok := cfg.Definitions[0].(*parser.Field); ok {
|
||||
b.variables[vdef.Name] = f.Value
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if vdef.DefaultValue != nil {
|
||||
if _, ok := b.variables[vdef.Name]; !ok || vdef.IsConst {
|
||||
b.variables[vdef.Name] = vdef.DefaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tree.Walk(processNode)
|
||||
}
|
||||
|
||||
func (b *Builder) evaluate(val parser.Value) parser.Value {
|
||||
switch v := val.(type) {
|
||||
case *parser.VariableReferenceValue:
|
||||
name := strings.TrimPrefix(v.Name, "@")
|
||||
if res, ok := b.variables[name]; ok {
|
||||
return b.evaluate(res)
|
||||
}
|
||||
return v
|
||||
case *parser.BinaryExpression:
|
||||
left := b.evaluate(v.Left)
|
||||
right := b.evaluate(v.Right)
|
||||
return b.compute(left, v.Operator, right)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value) parser.Value {
|
||||
if op.Type == parser.TokenConcat {
|
||||
s1 := b.valToString(left)
|
||||
s2 := b.valToString(right)
|
||||
return &parser.StringValue{Value: s1 + s2, Quoted: true}
|
||||
}
|
||||
|
||||
// Try Integer arithmetic first
|
||||
lI, lIsI := b.valToInt(left)
|
||||
rI, rIsI := b.valToInt(right)
|
||||
|
||||
if lIsI && rIsI {
|
||||
res := int64(0)
|
||||
switch op.Type {
|
||||
case parser.TokenPlus:
|
||||
res = lI + rI
|
||||
case parser.TokenMinus:
|
||||
res = lI - rI
|
||||
case parser.TokenStar:
|
||||
res = lI * rI
|
||||
case parser.TokenSlash:
|
||||
if rI != 0 {
|
||||
res = lI / rI
|
||||
}
|
||||
case parser.TokenPercent:
|
||||
if rI != 0 {
|
||||
res = lI % rI
|
||||
}
|
||||
case parser.TokenAmpersand:
|
||||
res = lI & rI
|
||||
case parser.TokenPipe:
|
||||
res = lI | rI
|
||||
case parser.TokenCaret:
|
||||
res = lI ^ rI
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (b *Builder) valToString(v parser.Value) string {
|
||||
switch val := v.(type) {
|
||||
case *parser.StringValue:
|
||||
return val.Value
|
||||
case *parser.IntValue:
|
||||
return val.Raw
|
||||
case *parser.FloatValue:
|
||||
return val.Raw
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) valToFloat(v parser.Value) (float64, bool) {
|
||||
switch val := v.(type) {
|
||||
case *parser.FloatValue:
|
||||
return val.Value, true
|
||||
case *parser.IntValue:
|
||||
return float64(val.Value), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (b *Builder) valToInt(v parser.Value) (int64, bool) {
|
||||
switch val := v.(type) {
|
||||
case *parser.IntValue:
|
||||
return val.Value, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
type Insertable struct {
|
||||
@@ -45,11 +45,8 @@ func Format(config *parser.Configuration, w io.Writer) {
|
||||
}
|
||||
|
||||
func fixComment(text string) string {
|
||||
if strings.HasPrefix(text, "//!") {
|
||||
if len(text) > 3 && text[3] != ' ' {
|
||||
return "//! " + text[3:]
|
||||
}
|
||||
} else if strings.HasPrefix(text, "//#") {
|
||||
if !strings.HasPrefix(text, "//!") {
|
||||
if strings.HasPrefix(text, "//#") {
|
||||
if len(text) > 3 && text[3] != ' ' {
|
||||
return "//# " + text[3:]
|
||||
}
|
||||
@@ -58,6 +55,7 @@ func fixComment(text string) string {
|
||||
return "// " + text[2:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -104,6 +102,18 @@ func (f *Formatter) formatDefinition(def parser.Definition, indent int) int {
|
||||
|
||||
fmt.Fprintf(f.writer, "%s}", indentStr)
|
||||
return d.Subnode.EndPosition.Line
|
||||
case *parser.VariableDefinition:
|
||||
macro := "#var"
|
||||
if d.IsConst {
|
||||
macro = "#let"
|
||||
}
|
||||
fmt.Fprintf(f.writer, "%s%s %s: %s", indentStr, macro, d.Name, d.TypeExpr)
|
||||
if d.DefaultValue != nil {
|
||||
fmt.Fprint(f.writer, " = ")
|
||||
endLine := f.formatValue(d.DefaultValue, indent)
|
||||
return endLine
|
||||
}
|
||||
return d.Position.Line
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -142,6 +152,18 @@ func (f *Formatter) formatValue(val parser.Value, indent int) int {
|
||||
case *parser.ReferenceValue:
|
||||
fmt.Fprint(f.writer, v.Value)
|
||||
return v.Position.Line
|
||||
case *parser.VariableReferenceValue:
|
||||
fmt.Fprint(f.writer, v.Name)
|
||||
return v.Position.Line
|
||||
case *parser.BinaryExpression:
|
||||
f.formatValue(v.Left, indent)
|
||||
fmt.Fprintf(f.writer, " %s ", v.Operator.Value)
|
||||
f.formatValue(v.Right, indent)
|
||||
return v.Position.Line
|
||||
case *parser.UnaryExpression:
|
||||
fmt.Fprint(f.writer, v.Operator.Value)
|
||||
f.formatValue(v.Right, indent)
|
||||
return v.Position.Line
|
||||
case *parser.ArrayValue:
|
||||
fmt.Fprint(f.writer, "{ ")
|
||||
for i, e := range v.Elements {
|
||||
|
||||
@@ -5,15 +5,22 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/logger"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/logger"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
type VariableInfo struct {
|
||||
Def *parser.VariableDefinition
|
||||
File string
|
||||
Doc string
|
||||
}
|
||||
|
||||
type ProjectTree struct {
|
||||
Root *ProjectNode
|
||||
References []Reference
|
||||
IsolatedFiles map[string]*ProjectNode
|
||||
GlobalPragmas map[string][]string
|
||||
NodeMap map[string][]*ProjectNode
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) ScanDirectory(rootPath string) error {
|
||||
@@ -22,13 +29,14 @@ func (pt *ProjectTree) ScanDirectory(rootPath string) error {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".marte") {
|
||||
logger.Printf("indexing: %s [%s]\n", info.Name(), path)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err // Or log and continue
|
||||
}
|
||||
p := parser.NewParser(string(content))
|
||||
config, err := p.Parse()
|
||||
if err == nil {
|
||||
config, _ := p.Parse()
|
||||
if config != nil {
|
||||
pt.AddFile(path, config)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +48,9 @@ type Reference struct {
|
||||
Name string
|
||||
Position parser.Position
|
||||
File string
|
||||
Target *ProjectNode // Resolved target
|
||||
Target *ProjectNode
|
||||
TargetVariable *parser.VariableDefinition
|
||||
IsVariable bool
|
||||
}
|
||||
|
||||
type ProjectNode struct {
|
||||
@@ -53,6 +63,7 @@ type ProjectNode struct {
|
||||
Metadata map[string]string // Store extra info like Class, Type, Size
|
||||
Target *ProjectNode // Points to referenced node (for Direct References/Links)
|
||||
Pragmas []string
|
||||
Variables map[string]VariableInfo
|
||||
}
|
||||
|
||||
type Fragment struct {
|
||||
@@ -69,6 +80,7 @@ func NewProjectTree() *ProjectTree {
|
||||
Root: &ProjectNode{
|
||||
Children: make(map[string]*ProjectNode),
|
||||
Metadata: make(map[string]string),
|
||||
Variables: make(map[string]VariableInfo),
|
||||
},
|
||||
IsolatedFiles: make(map[string]*ProjectNode),
|
||||
GlobalPragmas: make(map[string][]string),
|
||||
@@ -120,8 +132,11 @@ func (pt *ProjectTree) removeFileFromNode(node *ProjectNode, file string) {
|
||||
node.Metadata = make(map[string]string)
|
||||
pt.rebuildMetadata(node)
|
||||
|
||||
for _, child := range node.Children {
|
||||
for name, child := range node.Children {
|
||||
pt.removeFileFromNode(child, file)
|
||||
if len(child.Fragments) == 0 && len(child.Children) == 0 {
|
||||
delete(node.Children, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +188,7 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
|
||||
node := &ProjectNode{
|
||||
Children: make(map[string]*ProjectNode),
|
||||
Metadata: make(map[string]string),
|
||||
Variables: make(map[string]VariableInfo),
|
||||
}
|
||||
pt.IsolatedFiles[file] = node
|
||||
pt.populateNode(node, file, config)
|
||||
@@ -181,13 +197,8 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
|
||||
|
||||
node := pt.Root
|
||||
parts := strings.Split(config.Package.URI, ".")
|
||||
// Skip first part as per spec (Project Name is namespace only)
|
||||
startIdx := 0
|
||||
if len(parts) > 0 {
|
||||
startIdx = 1
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(parts); i++ {
|
||||
for i := 0; i < len(parts); i++ {
|
||||
part := strings.TrimSpace(parts[i])
|
||||
if part == "" {
|
||||
continue
|
||||
@@ -199,6 +210,7 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
|
||||
Children: make(map[string]*ProjectNode),
|
||||
Parent: node,
|
||||
Metadata: make(map[string]string),
|
||||
Variables: make(map[string]VariableInfo),
|
||||
}
|
||||
}
|
||||
node = node.Children[part]
|
||||
@@ -221,7 +233,11 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
|
||||
case *parser.Field:
|
||||
fileFragment.Definitions = append(fileFragment.Definitions, d)
|
||||
pt.indexValue(file, d.Value)
|
||||
case *parser.VariableDefinition:
|
||||
fileFragment.Definitions = append(fileFragment.Definitions, d)
|
||||
node.Variables[d.Name] = VariableInfo{Def: d, File: file, Doc: doc}
|
||||
case *parser.ObjectNode:
|
||||
fileFragment.Definitions = append(fileFragment.Definitions, d)
|
||||
norm := NormalizeName(d.Name)
|
||||
if _, ok := node.Children[norm]; !ok {
|
||||
node.Children[norm] = &ProjectNode{
|
||||
@@ -230,6 +246,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
|
||||
Children: make(map[string]*ProjectNode),
|
||||
Parent: node,
|
||||
Metadata: make(map[string]string),
|
||||
Variables: make(map[string]VariableInfo),
|
||||
}
|
||||
}
|
||||
child := node.Children[norm]
|
||||
@@ -275,7 +292,11 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
|
||||
frag.Definitions = append(frag.Definitions, d)
|
||||
pt.indexValue(file, d.Value)
|
||||
pt.extractFieldMetadata(node, d)
|
||||
case *parser.VariableDefinition:
|
||||
frag.Definitions = append(frag.Definitions, d)
|
||||
node.Variables[d.Name] = VariableInfo{Def: d, File: file, Doc: subDoc}
|
||||
case *parser.ObjectNode:
|
||||
frag.Definitions = append(frag.Definitions, d)
|
||||
norm := NormalizeName(d.Name)
|
||||
if _, ok := node.Children[norm]; !ok {
|
||||
node.Children[norm] = &ProjectNode{
|
||||
@@ -284,6 +305,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
|
||||
Children: make(map[string]*ProjectNode),
|
||||
Parent: node,
|
||||
Metadata: make(map[string]string),
|
||||
Variables: make(map[string]VariableInfo),
|
||||
}
|
||||
}
|
||||
child := node.Children[norm]
|
||||
@@ -379,6 +401,19 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
|
||||
Position: v.Position,
|
||||
File: file,
|
||||
})
|
||||
case *parser.VariableReferenceValue:
|
||||
name := strings.TrimPrefix(v.Name, "@")
|
||||
pt.References = append(pt.References, Reference{
|
||||
Name: name,
|
||||
Position: v.Position,
|
||||
File: file,
|
||||
IsVariable: true,
|
||||
})
|
||||
case *parser.BinaryExpression:
|
||||
pt.indexValue(file, v.Left)
|
||||
pt.indexValue(file, v.Right)
|
||||
case *parser.UnaryExpression:
|
||||
pt.indexValue(file, v.Right)
|
||||
case *parser.ArrayValue:
|
||||
for _, elem := range v.Elements {
|
||||
pt.indexValue(file, elem)
|
||||
@@ -386,39 +421,108 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) RebuildIndex() {
|
||||
pt.NodeMap = make(map[string][]*ProjectNode)
|
||||
visitor := func(n *ProjectNode) {
|
||||
pt.NodeMap[n.Name] = append(pt.NodeMap[n.Name], n)
|
||||
if n.RealName != n.Name {
|
||||
pt.NodeMap[n.RealName] = append(pt.NodeMap[n.RealName], n)
|
||||
}
|
||||
}
|
||||
pt.Walk(visitor)
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) ResolveReferences() {
|
||||
pt.RebuildIndex()
|
||||
for i := range pt.References {
|
||||
ref := &pt.References[i]
|
||||
if isoNode, ok := pt.IsolatedFiles[ref.File]; ok {
|
||||
ref.Target = pt.findNode(isoNode, ref.Name)
|
||||
} else {
|
||||
ref.Target = pt.findNode(pt.Root, ref.Name)
|
||||
|
||||
container := pt.GetNodeContaining(ref.File, ref.Position)
|
||||
|
||||
if v := pt.ResolveVariable(container, ref.Name); v != nil {
|
||||
ref.TargetVariable = v.Def
|
||||
continue
|
||||
}
|
||||
|
||||
ref.Target = pt.ResolveName(container, ref.Name, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) findNode(root *ProjectNode, name string) *ProjectNode {
|
||||
if root.RealName == name || root.Name == name {
|
||||
return root
|
||||
func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode {
|
||||
if pt.NodeMap == nil {
|
||||
pt.RebuildIndex()
|
||||
}
|
||||
|
||||
if strings.Contains(name, ".") {
|
||||
parts := strings.Split(name, ".")
|
||||
rootName := parts[0]
|
||||
|
||||
candidates := pt.NodeMap[rootName]
|
||||
|
||||
for _, cand := range candidates {
|
||||
if !pt.isDescendant(cand, root) {
|
||||
continue
|
||||
}
|
||||
|
||||
curr := cand
|
||||
valid := true
|
||||
for i := 1; i < len(parts); i++ {
|
||||
nextName := parts[i]
|
||||
normNext := NormalizeName(nextName)
|
||||
if child, ok := curr.Children[normNext]; ok {
|
||||
curr = child
|
||||
} else {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
if predicate == nil || predicate(curr) {
|
||||
return curr
|
||||
}
|
||||
for _, child := range root.Children {
|
||||
if res := pt.findNode(child, name); res != nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
candidates := pt.NodeMap[name]
|
||||
for _, cand := range candidates {
|
||||
if !pt.isDescendant(cand, root) {
|
||||
continue
|
||||
}
|
||||
if predicate == nil || predicate(cand) {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) isDescendant(node, root *ProjectNode) bool {
|
||||
if node == root {
|
||||
return true
|
||||
}
|
||||
if root == nil {
|
||||
return true
|
||||
}
|
||||
curr := node
|
||||
for curr != nil {
|
||||
if curr == root {
|
||||
return true
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Node *ProjectNode
|
||||
Field *parser.Field
|
||||
Reference *Reference
|
||||
Variable *parser.VariableDefinition
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) Query(file string, line, col int) *QueryResult {
|
||||
logger.Printf("File: %s:%d:%d", file, line, col)
|
||||
for i := range pt.References {
|
||||
logger.Printf("%s", pt.Root.Name)
|
||||
ref := &pt.References[i]
|
||||
if ref.File == file {
|
||||
if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) {
|
||||
@@ -464,6 +568,10 @@ func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int)
|
||||
if line == f.Position.Line && col >= f.Position.Column && col < f.Position.Column+len(f.Name) {
|
||||
return &QueryResult{Field: f}
|
||||
}
|
||||
} else if v, ok := def.(*parser.VariableDefinition); ok {
|
||||
if line == v.Position.Line {
|
||||
return &QueryResult{Variable: v}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,3 +625,34 @@ func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos pa
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) ResolveName(ctx *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode {
|
||||
if ctx == nil {
|
||||
return pt.FindNode(pt.Root, name, predicate)
|
||||
}
|
||||
|
||||
curr := ctx
|
||||
for curr != nil {
|
||||
if found := pt.FindNode(curr, name, predicate); found != nil {
|
||||
return found
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableInfo {
|
||||
curr := ctx
|
||||
for curr != nil {
|
||||
if v, ok := curr.Variables[name]; ok {
|
||||
return &v
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
if pt.Root != nil {
|
||||
if v, ok := pt.Root.Variables[name]; ok {
|
||||
return &v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,210 +0,0 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestInitProjectScan(t *testing.T) {
|
||||
// 1. Setup temp dir with files
|
||||
tmpDir, err := os.MkdirTemp("", "lsp_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// File 1: Definition
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "def.marte"), []byte("#package Test.Common\n+Target = { Class = C }"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// File 2: Reference
|
||||
// +Source = { Class = C Link = Target }
|
||||
// Link = Target starts at index ...
|
||||
// #package Test.Common (21 chars including newline)
|
||||
// +Source = { Class = C Link = Target }
|
||||
// 012345678901234567890123456789012345
|
||||
// Previous offset was 29.
|
||||
// Now add 21?
|
||||
// #package Test.Common\n
|
||||
// +Source = ...
|
||||
// So add 21 to Character? Or Line 1?
|
||||
// It's on Line 1 (0-based 1).
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "ref.marte"), []byte("#package Test.Common\n+Source = { Class = C Link = Target }"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 2. Initialize
|
||||
tree = index.NewProjectTree() // Reset global tree
|
||||
|
||||
initParams := InitializeParams{RootPath: tmpDir}
|
||||
paramsBytes, _ := json.Marshal(initParams)
|
||||
|
||||
msg := &JsonRpcMessage{
|
||||
Method: "initialize",
|
||||
Params: paramsBytes,
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
handleMessage(msg)
|
||||
|
||||
// Query the reference in ref.marte at "Target"
|
||||
// Target starts at index 29 (0-based) on Line 1
|
||||
defParams := DefinitionParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: "file://" + filepath.Join(tmpDir, "ref.marte")},
|
||||
Position: Position{Line: 1, Character: 29},
|
||||
}
|
||||
|
||||
res := handleDefinition(defParams)
|
||||
if res == nil {
|
||||
t.Fatal("Definition not found via LSP after initialization")
|
||||
}
|
||||
|
||||
locs, ok := res.([]Location)
|
||||
if !ok {
|
||||
t.Fatalf("Expected []Location, got %T", res)
|
||||
}
|
||||
|
||||
if len(locs) == 0 {
|
||||
t.Fatal("No locations found")
|
||||
}
|
||||
|
||||
// Verify uri points to def.marte
|
||||
expectedURI := "file://" + filepath.Join(tmpDir, "def.marte")
|
||||
if locs[0].URI != expectedURI {
|
||||
t.Errorf("Expected URI %s, got %s", expectedURI, locs[0].URI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDefinition(t *testing.T) {
|
||||
// Reset tree for test
|
||||
tree = index.NewProjectTree()
|
||||
|
||||
content := `
|
||||
+MyObject = {
|
||||
Class = Type
|
||||
}
|
||||
+RefObject = {
|
||||
Class = Type
|
||||
RefField = MyObject
|
||||
}
|
||||
`
|
||||
path := "/test.marte"
|
||||
p := parser.NewParser(content)
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
tree.AddFile(path, config)
|
||||
tree.ResolveReferences()
|
||||
|
||||
t.Logf("Refs: %d", len(tree.References))
|
||||
for _, r := range tree.References {
|
||||
t.Logf(" %s at %d:%d", r.Name, r.Position.Line, r.Position.Column)
|
||||
}
|
||||
|
||||
// Test Go to Definition on MyObject reference
|
||||
params := DefinitionParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: Position{Line: 6, Character: 15}, // "MyObject" in RefField = MyObject
|
||||
}
|
||||
|
||||
result := handleDefinition(params)
|
||||
if result == nil {
|
||||
t.Fatal("handleDefinition returned nil")
|
||||
}
|
||||
|
||||
locations, ok := result.([]Location)
|
||||
if !ok {
|
||||
t.Fatalf("Expected []Location, got %T", result)
|
||||
}
|
||||
|
||||
if len(locations) != 1 {
|
||||
t.Fatalf("Expected 1 location, got %d", len(locations))
|
||||
}
|
||||
|
||||
if locations[0].Range.Start.Line != 1 { // +MyObject is on line 2 (0-indexed 1)
|
||||
t.Errorf("Expected definition on line 1, got %d", locations[0].Range.Start.Line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleReferences(t *testing.T) {
|
||||
// Reset tree for test
|
||||
tree = index.NewProjectTree()
|
||||
|
||||
content := `
|
||||
+MyObject = {
|
||||
Class = Type
|
||||
}
|
||||
+RefObject = {
|
||||
Class = Type
|
||||
RefField = MyObject
|
||||
}
|
||||
+AnotherRef = {
|
||||
Ref = MyObject
|
||||
}
|
||||
`
|
||||
path := "/test.marte"
|
||||
p := parser.NewParser(content)
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
tree.AddFile(path, config)
|
||||
tree.ResolveReferences()
|
||||
|
||||
// Test Find References for MyObject (triggered from its definition)
|
||||
params := ReferenceParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: Position{Line: 1, Character: 1}, // "+MyObject"
|
||||
Context: ReferenceContext{IncludeDeclaration: true},
|
||||
}
|
||||
|
||||
locations := handleReferences(params)
|
||||
if len(locations) != 3 { // 1 declaration + 2 references
|
||||
t.Fatalf("Expected 3 locations, got %d", len(locations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLSPFormatting(t *testing.T) {
|
||||
// Setup
|
||||
content := `
|
||||
#package Proj.Main
|
||||
+Object={
|
||||
Field=1
|
||||
}
|
||||
`
|
||||
uri := "file:///test.marte"
|
||||
|
||||
// Open (populate documents map)
|
||||
documents[uri] = content
|
||||
|
||||
// Format
|
||||
params := DocumentFormattingParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: uri},
|
||||
}
|
||||
|
||||
edits := handleFormatting(params)
|
||||
|
||||
if len(edits) != 1 {
|
||||
t.Fatalf("Expected 1 edit, got %d", len(edits))
|
||||
}
|
||||
|
||||
newText := edits[0].NewText
|
||||
|
||||
expected := `#package Proj.Main
|
||||
|
||||
+Object = {
|
||||
Field = 1
|
||||
}
|
||||
`
|
||||
// Normalize newlines for comparison just in case
|
||||
if strings.TrimSpace(strings.ReplaceAll(newText, "\r\n", "\n")) != strings.TrimSpace(strings.ReplaceAll(expected, "\r\n", "\n")) {
|
||||
t.Errorf("Formatting mismatch.\nExpected:\n%s\nGot:\n%s", expected, newText)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ type Subnode struct {
|
||||
Definitions []Definition
|
||||
}
|
||||
|
||||
func (s *Subnode) Pos() Position { return s.Position }
|
||||
|
||||
type Value interface {
|
||||
Node
|
||||
isValue()
|
||||
@@ -115,7 +117,49 @@ type Comment struct {
|
||||
Doc bool // true if starts with //#
|
||||
}
|
||||
|
||||
func (c *Comment) Pos() Position { return c.Position }
|
||||
|
||||
type Pragma struct {
|
||||
Position Position
|
||||
Text string
|
||||
}
|
||||
|
||||
func (p *Pragma) Pos() Position { return p.Position }
|
||||
|
||||
type VariableDefinition struct {
|
||||
Position Position
|
||||
Name string
|
||||
TypeExpr string
|
||||
DefaultValue Value
|
||||
IsConst bool
|
||||
}
|
||||
|
||||
func (v *VariableDefinition) Pos() Position { return v.Position }
|
||||
func (v *VariableDefinition) isDefinition() {}
|
||||
|
||||
type VariableReferenceValue struct {
|
||||
Position Position
|
||||
Name string
|
||||
}
|
||||
|
||||
func (v *VariableReferenceValue) Pos() Position { return v.Position }
|
||||
func (v *VariableReferenceValue) isValue() {}
|
||||
|
||||
type BinaryExpression struct {
|
||||
Position Position
|
||||
Left Value
|
||||
Operator Token
|
||||
Right Value
|
||||
}
|
||||
|
||||
func (b *BinaryExpression) Pos() Position { return b.Position }
|
||||
func (b *BinaryExpression) isValue() {}
|
||||
|
||||
type UnaryExpression struct {
|
||||
Position Position
|
||||
Operator Token
|
||||
Right Value
|
||||
}
|
||||
|
||||
func (u *UnaryExpression) Pos() Position { return u.Position }
|
||||
func (u *UnaryExpression) isValue() {}
|
||||
|
||||
@@ -20,9 +20,24 @@ const (
|
||||
TokenBool
|
||||
TokenPackage
|
||||
TokenPragma
|
||||
TokenLet
|
||||
TokenComment
|
||||
TokenDocstring
|
||||
TokenComma
|
||||
TokenColon
|
||||
TokenPipe
|
||||
TokenLBracket
|
||||
TokenRBracket
|
||||
TokenSymbol
|
||||
TokenPlus
|
||||
TokenMinus
|
||||
TokenStar
|
||||
TokenSlash
|
||||
TokenPercent
|
||||
TokenCaret
|
||||
TokenAmpersand
|
||||
TokenConcat
|
||||
TokenVariableReference
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
@@ -124,14 +139,49 @@ func (l *Lexer) NextToken() Token {
|
||||
return l.emit(TokenRBrace)
|
||||
case ',':
|
||||
return l.emit(TokenComma)
|
||||
case ':':
|
||||
return l.emit(TokenColon)
|
||||
case '|':
|
||||
return l.emit(TokenPipe)
|
||||
case '[':
|
||||
return l.emit(TokenLBracket)
|
||||
case ']':
|
||||
return l.emit(TokenRBracket)
|
||||
case '+':
|
||||
if unicode.IsSpace(l.peek()) || unicode.IsDigit(l.peek()) {
|
||||
return l.emit(TokenPlus)
|
||||
}
|
||||
return l.lexObjectIdentifier()
|
||||
case '-':
|
||||
return l.emit(TokenMinus)
|
||||
case '*':
|
||||
return l.emit(TokenStar)
|
||||
case '/':
|
||||
p := l.peek()
|
||||
if p == '/' || p == '*' || p == '#' || p == '!' {
|
||||
return l.lexComment()
|
||||
}
|
||||
return l.emit(TokenSlash)
|
||||
case '%':
|
||||
return l.emit(TokenPercent)
|
||||
case '^':
|
||||
return l.emit(TokenCaret)
|
||||
case '&':
|
||||
return l.emit(TokenAmpersand)
|
||||
case '.':
|
||||
if l.peek() == '.' {
|
||||
l.next()
|
||||
return l.emit(TokenConcat)
|
||||
}
|
||||
return l.emit(TokenSymbol)
|
||||
case '~', '!', '<', '>', '(', ')', '?', '\\':
|
||||
return l.emit(TokenSymbol)
|
||||
case '"':
|
||||
return l.lexString()
|
||||
case '/':
|
||||
return l.lexComment()
|
||||
case '#':
|
||||
return l.lexPackage()
|
||||
case '+':
|
||||
fallthrough
|
||||
return l.lexHashIdentifier()
|
||||
case '@':
|
||||
return l.lexVariableReference()
|
||||
case '$':
|
||||
return l.lexObjectIdentifier()
|
||||
}
|
||||
@@ -151,7 +201,7 @@ func (l *Lexer) NextToken() Token {
|
||||
func (l *Lexer) lexIdentifier() Token {
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' {
|
||||
continue
|
||||
}
|
||||
l.backup()
|
||||
@@ -187,13 +237,64 @@ func (l *Lexer) lexString() Token {
|
||||
}
|
||||
|
||||
func (l *Lexer) lexNumber() Token {
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsDigit(r) || unicode.IsLetter(r) || r == '.' || r == '-' || r == '+' {
|
||||
continue
|
||||
}
|
||||
l.backup()
|
||||
// Check for hex or binary prefix if we started with '0'
|
||||
if l.input[l.start:l.pos] == "0" {
|
||||
switch l.peek() {
|
||||
case 'x', 'X':
|
||||
l.next()
|
||||
l.lexHexDigits()
|
||||
return l.emit(TokenNumber)
|
||||
case 'b', 'B':
|
||||
l.next()
|
||||
l.lexBinaryDigits()
|
||||
return l.emit(TokenNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// Consume remaining digits
|
||||
l.lexDigits()
|
||||
|
||||
if l.peek() == '.' {
|
||||
l.next()
|
||||
l.lexDigits()
|
||||
}
|
||||
|
||||
if r := l.peek(); r == 'e' || r == 'E' {
|
||||
l.next()
|
||||
if p := l.peek(); p == '+' || p == '-' {
|
||||
l.next()
|
||||
}
|
||||
l.lexDigits()
|
||||
}
|
||||
|
||||
return l.emit(TokenNumber)
|
||||
}
|
||||
|
||||
func (l *Lexer) lexHexDigits() {
|
||||
for {
|
||||
r := l.peek()
|
||||
if unicode.IsDigit(r) || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') {
|
||||
l.next()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) lexBinaryDigits() {
|
||||
for {
|
||||
r := l.peek()
|
||||
if r == '0' || r == '1' {
|
||||
l.next()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) lexDigits() {
|
||||
for unicode.IsDigit(l.peek()) {
|
||||
l.next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,18 +344,33 @@ 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
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsLetter(r) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == '#' {
|
||||
continue
|
||||
}
|
||||
l.backup()
|
||||
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.emit(TokenError)
|
||||
if val == "#let" {
|
||||
return l.emit(TokenLet)
|
||||
}
|
||||
return l.emit(TokenIdentifier)
|
||||
}
|
||||
|
||||
func (l *Lexer) lexVariableReference() Token {
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
|
||||
continue
|
||||
}
|
||||
l.backup()
|
||||
return l.emit(TokenVariableReference)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type Parser struct {
|
||||
buf []Token
|
||||
comments []Comment
|
||||
pragmas []Pragma
|
||||
errors []error
|
||||
}
|
||||
|
||||
func NewParser(input string) *Parser {
|
||||
@@ -19,6 +20,10 @@ func NewParser(input string) *Parser {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) addError(pos Position, msg string) {
|
||||
p.errors = append(p.errors, fmt.Errorf("%d:%d: %s", pos.Line, pos.Column, msg))
|
||||
}
|
||||
|
||||
func (p *Parser) next() Token {
|
||||
if len(p.buf) > 0 {
|
||||
t := p.buf[0]
|
||||
@@ -71,72 +76,87 @@ func (p *Parser) Parse() (*Configuration, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
def, err := p.parseDefinition()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
def, ok := p.parseDefinition()
|
||||
if ok {
|
||||
config.Definitions = append(config.Definitions, def)
|
||||
} else {
|
||||
// Synchronization: skip token if not consumed to make progress
|
||||
if p.peek() == tok {
|
||||
p.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Comments = p.comments
|
||||
config.Pragmas = p.pragmas
|
||||
return config, nil
|
||||
|
||||
var err error
|
||||
if len(p.errors) > 0 {
|
||||
err = p.errors[0]
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (p *Parser) parseDefinition() (Definition, error) {
|
||||
func (p *Parser) parseDefinition() (Definition, bool) {
|
||||
tok := p.next()
|
||||
switch tok.Type {
|
||||
case TokenLet:
|
||||
return p.parseLet(tok)
|
||||
case TokenIdentifier:
|
||||
// Could be Field = Value OR Node = { ... }
|
||||
name := tok.Value
|
||||
if p.next().Type != TokenEqual {
|
||||
return nil, fmt.Errorf("%d:%d: expected =", tok.Position.Line, tok.Position.Column)
|
||||
if name == "#var" {
|
||||
return p.parseVariableDefinition(tok)
|
||||
}
|
||||
if p.peek().Type != TokenEqual {
|
||||
p.addError(tok.Position, "expected =")
|
||||
return nil, false
|
||||
}
|
||||
p.next() // Consume =
|
||||
|
||||
// Disambiguate based on RHS
|
||||
nextTok := p.peek()
|
||||
if nextTok.Type == TokenLBrace {
|
||||
// Check if it looks like a Subnode (contains definitions) or Array (contains values)
|
||||
if p.isSubnodeLookahead() {
|
||||
sub, err := p.parseSubnode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
sub, ok := p.parseSubnode()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &ObjectNode{
|
||||
Position: tok.Position,
|
||||
Name: name,
|
||||
Subnode: sub,
|
||||
}, nil
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// Default to Field
|
||||
val, err := p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
val, ok := p.parseValue()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &Field{
|
||||
Position: tok.Position,
|
||||
Name: name,
|
||||
Value: val,
|
||||
}, nil
|
||||
}, true
|
||||
|
||||
case TokenObjectIdentifier:
|
||||
// node = subnode
|
||||
name := tok.Value
|
||||
if p.next().Type != TokenEqual {
|
||||
return nil, fmt.Errorf("%d:%d: expected =", tok.Position.Line, tok.Position.Column)
|
||||
if p.peek().Type != TokenEqual {
|
||||
p.addError(tok.Position, "expected =")
|
||||
return nil, false
|
||||
}
|
||||
sub, err := p.parseSubnode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
p.next() // Consume =
|
||||
|
||||
sub, ok := p.parseSubnode()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &ObjectNode{
|
||||
Position: tok.Position,
|
||||
Name: name,
|
||||
Subnode: sub,
|
||||
}, nil
|
||||
}, true
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unexpected token %v", tok.Position.Line, tok.Position.Column, tok.Value)
|
||||
p.addError(tok.Position, fmt.Sprintf("unexpected token %v", tok.Value))
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +196,11 @@ func (p *Parser) isSubnodeLookahead() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) parseSubnode() (Subnode, error) {
|
||||
func (p *Parser) parseSubnode() (Subnode, bool) {
|
||||
tok := p.next()
|
||||
if tok.Type != TokenLBrace {
|
||||
return Subnode{}, fmt.Errorf("%d:%d: expected {", tok.Position.Line, tok.Position.Column)
|
||||
p.addError(tok.Position, "expected {")
|
||||
return Subnode{}, false
|
||||
}
|
||||
sub := Subnode{Position: tok.Position}
|
||||
for {
|
||||
@@ -190,18 +211,73 @@ func (p *Parser) parseSubnode() (Subnode, error) {
|
||||
break
|
||||
}
|
||||
if t.Type == TokenEOF {
|
||||
return sub, fmt.Errorf("%d:%d: unexpected EOF, expected }", t.Position.Line, t.Position.Column)
|
||||
}
|
||||
def, err := p.parseDefinition()
|
||||
if err != nil {
|
||||
return sub, err
|
||||
p.addError(t.Position, "unexpected EOF, expected }")
|
||||
sub.EndPosition = t.Position
|
||||
return sub, true
|
||||
}
|
||||
def, ok := p.parseDefinition()
|
||||
if ok {
|
||||
sub.Definitions = append(sub.Definitions, def)
|
||||
} else {
|
||||
if p.peek() == t {
|
||||
p.next()
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
}
|
||||
return sub, true
|
||||
}
|
||||
|
||||
func (p *Parser) parseValue() (Value, error) {
|
||||
func (p *Parser) parseValue() (Value, bool) {
|
||||
return p.parseExpression(0)
|
||||
}
|
||||
|
||||
func getPrecedence(t TokenType) int {
|
||||
switch t {
|
||||
case TokenStar, TokenSlash, TokenPercent:
|
||||
return 5
|
||||
case TokenPlus, TokenMinus:
|
||||
return 4
|
||||
case TokenConcat:
|
||||
return 3
|
||||
case TokenAmpersand:
|
||||
return 2
|
||||
case TokenPipe, TokenCaret:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) parseExpression(minPrecedence int) (Value, bool) {
|
||||
left, ok := p.parseAtom()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for {
|
||||
t := p.peek()
|
||||
prec := getPrecedence(t.Type)
|
||||
if prec == 0 || prec <= minPrecedence {
|
||||
break
|
||||
}
|
||||
p.next()
|
||||
|
||||
right, ok := p.parseExpression(prec)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
left = &BinaryExpression{
|
||||
Position: left.Pos(),
|
||||
Left: left,
|
||||
Operator: t,
|
||||
Right: right,
|
||||
}
|
||||
}
|
||||
return left, true
|
||||
}
|
||||
|
||||
func (p *Parser) parseAtom() (Value, bool) {
|
||||
tok := p.next()
|
||||
switch tok.Type {
|
||||
case TokenString:
|
||||
@@ -209,24 +285,55 @@ func (p *Parser) parseValue() (Value, error) {
|
||||
Position: tok.Position,
|
||||
Value: strings.Trim(tok.Value, "\""),
|
||||
Quoted: true,
|
||||
}, nil
|
||||
}, true
|
||||
|
||||
case TokenNumber:
|
||||
// Simplistic handling
|
||||
if strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") {
|
||||
isFloat := (strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") || strings.Contains(tok.Value, "E")) &&
|
||||
!strings.HasPrefix(tok.Value, "0x") && !strings.HasPrefix(tok.Value, "0X") &&
|
||||
!strings.HasPrefix(tok.Value, "0b") && !strings.HasPrefix(tok.Value, "0B")
|
||||
|
||||
if isFloat {
|
||||
f, _ := strconv.ParseFloat(tok.Value, 64)
|
||||
return &FloatValue{Position: tok.Position, Value: f, Raw: tok.Value}, nil
|
||||
return &FloatValue{Position: tok.Position, Value: f, Raw: tok.Value}, true
|
||||
}
|
||||
i, _ := strconv.ParseInt(tok.Value, 0, 64)
|
||||
return &IntValue{Position: tok.Position, Value: i, Raw: tok.Value}, nil
|
||||
return &IntValue{Position: tok.Position, Value: i, Raw: tok.Value}, true
|
||||
case TokenBool:
|
||||
return &BoolValue{Position: tok.Position, Value: tok.Value == "true"},
|
||||
nil
|
||||
true
|
||||
case TokenIdentifier:
|
||||
// reference?
|
||||
return &ReferenceValue{Position: tok.Position, Value: tok.Value}, nil
|
||||
return &ReferenceValue{Position: tok.Position, Value: tok.Value}, true
|
||||
case TokenVariableReference:
|
||||
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
|
||||
case TokenMinus:
|
||||
val, ok := p.parseAtom()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &UnaryExpression{Position: tok.Position, Operator: tok, Right: val}, true
|
||||
case TokenObjectIdentifier:
|
||||
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
|
||||
case TokenSymbol:
|
||||
if tok.Value == "(" {
|
||||
val, ok := p.parseExpression(0)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if next := p.next(); next.Type != TokenSymbol || next.Value != ")" {
|
||||
p.addError(next.Position, "expected )")
|
||||
return nil, false
|
||||
}
|
||||
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:
|
||||
// array
|
||||
arr := &ArrayValue{Position: tok.Position}
|
||||
for {
|
||||
t := p.peek()
|
||||
@@ -239,14 +346,127 @@ func (p *Parser) parseValue() (Value, error) {
|
||||
p.next()
|
||||
continue
|
||||
}
|
||||
val, err := p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
val, ok := p.parseValue()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
arr.Elements = append(arr.Elements, val)
|
||||
}
|
||||
return arr, nil
|
||||
return arr, true
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unexpected value token %v", tok.Position.Line, tok.Position.Column, tok.Value)
|
||||
p.addError(tok.Position, fmt.Sprintf("unexpected value token %v", tok.Value))
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) parseVariableDefinition(startTok Token) (Definition, bool) {
|
||||
nameTok := p.next()
|
||||
if nameTok.Type != TokenIdentifier {
|
||||
p.addError(nameTok.Position, "expected variable name")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if p.next().Type != TokenColon {
|
||||
p.addError(nameTok.Position, "expected :")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var typeTokens []Token
|
||||
startLine := nameTok.Position.Line
|
||||
|
||||
for {
|
||||
t := p.peek()
|
||||
if t.Position.Line > startLine || t.Type == TokenEOF {
|
||||
break
|
||||
}
|
||||
if t.Type == TokenEqual {
|
||||
if p.peekN(1).Type == TokenSymbol && p.peekN(1).Value == "~" {
|
||||
p.next()
|
||||
p.next()
|
||||
typeTokens = append(typeTokens, Token{Type: TokenSymbol, Value: "=~", Position: t.Position})
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
typeTokens = append(typeTokens, p.next())
|
||||
}
|
||||
|
||||
typeExpr := ""
|
||||
for _, t := range typeTokens {
|
||||
typeExpr += t.Value + " "
|
||||
}
|
||||
|
||||
var defVal Value
|
||||
if p.peek().Type == TokenEqual {
|
||||
p.next()
|
||||
val, ok := p.parseValue()
|
||||
if ok {
|
||||
defVal = val
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return &VariableDefinition{
|
||||
Position: startTok.Position,
|
||||
Name: nameTok.Value,
|
||||
TypeExpr: strings.TrimSpace(typeExpr),
|
||||
DefaultValue: defVal,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (p *Parser) parseLet(startTok Token) (Definition, bool) {
|
||||
nameTok := p.next()
|
||||
if nameTok.Type != TokenIdentifier {
|
||||
p.addError(nameTok.Position, "expected constant name")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if p.next().Type != TokenColon {
|
||||
p.addError(nameTok.Position, "expected :")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var typeTokens []Token
|
||||
startLine := nameTok.Position.Line
|
||||
|
||||
for {
|
||||
t := p.peek()
|
||||
if t.Position.Line > startLine || t.Type == TokenEOF {
|
||||
break
|
||||
}
|
||||
if t.Type == TokenEqual {
|
||||
break
|
||||
}
|
||||
typeTokens = append(typeTokens, p.next())
|
||||
}
|
||||
|
||||
typeExpr := ""
|
||||
for _, t := range typeTokens {
|
||||
typeExpr += t.Value + " "
|
||||
}
|
||||
|
||||
var defVal Value
|
||||
if p.next().Type != TokenEqual {
|
||||
p.addError(nameTok.Position, "expected =")
|
||||
return nil, false
|
||||
}
|
||||
val, ok := p.parseValue()
|
||||
if ok {
|
||||
defVal = val
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &VariableDefinition{
|
||||
Position: startTok.Position,
|
||||
Name: nameTok.Value,
|
||||
TypeExpr: strings.TrimSpace(typeExpr),
|
||||
DefaultValue: defVal,
|
||||
IsConst: true,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (p *Parser) Errors() []error {
|
||||
return p.errors
|
||||
}
|
||||
|
||||
447
internal/schema/marte.cue
Normal file
447
internal/schema/marte.cue
Normal file
@@ -0,0 +1,447 @@
|
||||
package schema
|
||||
|
||||
#Classes: {
|
||||
RealTimeApplication: {
|
||||
Functions!: {
|
||||
Class: "ReferenceContainer"
|
||||
[_= !~"^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: {
|
||||
...
|
||||
}
|
||||
StateMachineEvent: {
|
||||
NextState!: string
|
||||
NextStateError!: string
|
||||
Timeout?: uint32
|
||||
[_= !~"^(Class|NextState|Timeout|NextStateError|[#_$].+)$"]: Message
|
||||
...
|
||||
}
|
||||
_State: {
|
||||
Class: "ReferenceContainer"
|
||||
ENTER?: {
|
||||
Class: "ReferenceContainer"
|
||||
...
|
||||
}
|
||||
[_ = !~"^(Class|ENTER|EXIT)$"]: StateMachineEvent
|
||||
...
|
||||
}
|
||||
StateMachine: {
|
||||
[_ = !~"^(Class|[$].*)$"]: _State
|
||||
...
|
||||
}
|
||||
RealTimeState: {
|
||||
Threads: {...} // type: node
|
||||
...
|
||||
}
|
||||
RealTimeThread: {
|
||||
Functions: [...] // type: array
|
||||
...
|
||||
}
|
||||
GAMScheduler: {
|
||||
TimingDataSource: string // type: reference
|
||||
#meta: type: "scheduler"
|
||||
...
|
||||
}
|
||||
TimingDataSource: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
IOGAM: {
|
||||
InputSignals?: {...} // type: node
|
||||
OutputSignals?: {...} // type: node
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
ReferenceContainer: {
|
||||
...
|
||||
}
|
||||
ConstantGAM: {
|
||||
...
|
||||
#meta: type: "gam"
|
||||
}
|
||||
PIDGAM: {
|
||||
Kp: float | int // type: float (allow int as it promotes)
|
||||
Ki: float | int
|
||||
Kd: float | int
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
FileDataSource: {
|
||||
Filename: string
|
||||
Format?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
LoggerDataSource: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
DANStream: {
|
||||
Timeout?: int
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
EPICSCAInput: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
EPICSCAOutput: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
EPICSPVAInput: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
EPICSPVAOutput: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
SDNSubscriber: {
|
||||
ExecutionMode?: *"IndependentThread" | "RealTimeThread"
|
||||
Topic!: string
|
||||
Address?: string
|
||||
Interface!: string
|
||||
CPUs?: uint32
|
||||
InternalTimeout?: uint32
|
||||
Timeout?: uint32
|
||||
IgnoreTimeoutError?: 0 | 1
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
SDNPublisher: {
|
||||
Address: string
|
||||
Port: int
|
||||
Interface?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
UDPReceiver: {
|
||||
Port: int
|
||||
Address?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
UDPSender: {
|
||||
Destination: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
FileReader: {
|
||||
Filename: string
|
||||
Format?: string
|
||||
Interpolate?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
FileWriter: {
|
||||
Filename: string
|
||||
Format?: string
|
||||
StoreOnTrigger?: int
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
OrderedClass: {
|
||||
First: int
|
||||
Second: string
|
||||
...
|
||||
}
|
||||
BaseLib2GAM: {
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
ConversionGAM: {
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
DoubleHandshakeGAM: {
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
FilterGAM: {
|
||||
Num: [...]
|
||||
Den: [...]
|
||||
ResetInEachState?: _
|
||||
InputSignals?: {...}
|
||||
OutputSignals?: {...}
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
HistogramGAM: {
|
||||
BeginCycleNumber?: int
|
||||
StateChangeResetName?: string
|
||||
InputSignals?: {...}
|
||||
OutputSignals?: {...}
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
Interleaved2FlatGAM: {
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
FlattenedStructIOGAM: {
|
||||
#meta: type: "gam"
|
||||
...
|
||||
}
|
||||
MathExpressionGAM: {
|
||||
Expression: string
|
||||
InputSignals?: {...}
|
||||
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"
|
||||
...
|
||||
}
|
||||
DAN: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
LinuxTimer: {
|
||||
ExecutionMode?: string
|
||||
SleepNature?: string
|
||||
SleepPercentage?: _
|
||||
Phase?: int
|
||||
CPUMask?: int
|
||||
TimeProvider?: {...}
|
||||
Signals: {...}
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
LinkDataSource: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
MDSReader: {
|
||||
TreeName: string
|
||||
ShotNumber: int
|
||||
Frequency: float | int
|
||||
Signals: {...}
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
MDSWriter: {
|
||||
NumberOfBuffers: int
|
||||
CPUMask: int
|
||||
StackSize: int
|
||||
TreeName: string
|
||||
PulseNumber?: int
|
||||
StoreOnTrigger: int
|
||||
EventName: string
|
||||
TimeRefresh: float | int
|
||||
NumberOfPreTriggers?: int
|
||||
NumberOfPostTriggers?: int
|
||||
Signals: {...}
|
||||
Messages?: {...}
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI1588TimeStamp: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI6259ADC: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI6259DAC: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI6259DIO: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI6368ADC: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI6368DAC: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI6368DIO: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI9157CircularFifoReader: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
NI9157MxiDataSource: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
OPCUADSInput: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
OPCUADSOutput: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
RealTimeThreadAsyncBridge: {
|
||||
#meta: direction: "INOUT"
|
||||
#meta: multithreaded: bool | true
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
RealTimeThreadSynchronisation: {...}
|
||||
UARTDataSource: {
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
BaseLib2Wrapper: {...}
|
||||
EPICSCAClient: {...}
|
||||
EPICSPVA: {...}
|
||||
MemoryGate: {...}
|
||||
OPCUA: {...}
|
||||
SysLogger: {...}
|
||||
GAMDataSource: {
|
||||
#meta: multithreaded: false
|
||||
#meta: direction: "INOUT"
|
||||
#meta: type: "datasource"
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
#Meta: {
|
||||
direction?: "IN" | "OUT" | "INOUT"
|
||||
multithreaded?: bool
|
||||
...
|
||||
}
|
||||
|
||||
// Definition for any Object.
|
||||
// It must have a Class field.
|
||||
// Based on Class, it validates against #Classes.
|
||||
#Object: {
|
||||
Class: string
|
||||
"#meta"?: #Meta
|
||||
// Allow any other field by default (extensibility),
|
||||
// unless #Classes definition is closed.
|
||||
// We allow open structs now.
|
||||
...
|
||||
|
||||
// Unify if Class is known.
|
||||
// If Class is NOT in #Classes, this might fail or do nothing depending on CUE logic.
|
||||
// Actually, `#Classes[Class]` fails if key is missing.
|
||||
// This ensures we validate against known classes.
|
||||
// If we want to allow unknown classes, we need a check.
|
||||
// But spec implies validation should check known classes.
|
||||
#Classes[Class]
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
{
|
||||
"classes": {
|
||||
"RealTimeApplication": {
|
||||
"fields": [
|
||||
{"name": "Functions", "type": "node", "mandatory": true},
|
||||
{"name": "Data", "type": "node", "mandatory": true},
|
||||
{"name": "States", "type": "node", "mandatory": true}
|
||||
]
|
||||
},
|
||||
"StateMachine": {
|
||||
"fields": [
|
||||
{"name": "States", "type": "node", "mandatory": false}
|
||||
]
|
||||
},
|
||||
"RealTimeState": {
|
||||
"fields": [
|
||||
{"name": "Threads", "type": "node", "mandatory": true}
|
||||
]
|
||||
},
|
||||
"RealTimeThread": {
|
||||
"fields": [
|
||||
{"name": "Functions", "type": "array", "mandatory": true}
|
||||
]
|
||||
},
|
||||
"GAMScheduler": {
|
||||
"fields": [
|
||||
{"name": "TimingDataSource", "type": "reference", "mandatory": true}
|
||||
]
|
||||
},
|
||||
"TimingDataSource": {
|
||||
"fields": [],
|
||||
"direction": "IN"
|
||||
},
|
||||
"IOGAM": {
|
||||
"fields": [
|
||||
{"name": "InputSignals", "type": "node", "mandatory": false},
|
||||
{"name": "OutputSignals", "type": "node", "mandatory": false}
|
||||
]
|
||||
},
|
||||
"ReferenceContainer": {
|
||||
"fields": []
|
||||
},
|
||||
"ConstantGAM": {
|
||||
"fields": []
|
||||
},
|
||||
"PIDGAM": {
|
||||
"fields": [
|
||||
{"name": "Kp", "type": "float", "mandatory": true},
|
||||
{"name": "Ki", "type": "float", "mandatory": true},
|
||||
{"name": "Kd", "type": "float", "mandatory": true}
|
||||
]
|
||||
},
|
||||
"FileDataSource": {
|
||||
"fields": [
|
||||
{"name": "Filename", "type": "string", "mandatory": true},
|
||||
{"name": "Format", "type": "string", "mandatory": false}
|
||||
],
|
||||
"direction": "INOUT"
|
||||
},
|
||||
"LoggerDataSource": {
|
||||
"fields": [],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"DANStream": {
|
||||
"fields": [
|
||||
{"name": "Timeout", "type": "int", "mandatory": false}
|
||||
],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"EPICSCAInput": {
|
||||
"fields": [],
|
||||
"direction": "IN"
|
||||
},
|
||||
"EPICSCAOutput": {
|
||||
"fields": [],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"EPICSPVAInput": {
|
||||
"fields": [],
|
||||
"direction": "IN"
|
||||
},
|
||||
"EPICSPVAOutput": {
|
||||
"fields": [],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"SDNSubscriber": {
|
||||
"fields": [
|
||||
{"name": "Address", "type": "string", "mandatory": true},
|
||||
{"name": "Port", "type": "int", "mandatory": true},
|
||||
{"name": "Interface", "type": "string", "mandatory": false}
|
||||
],
|
||||
"direction": "IN"
|
||||
},
|
||||
"SDNPublisher": {
|
||||
"fields": [
|
||||
{"name": "Address", "type": "string", "mandatory": true},
|
||||
{"name": "Port", "type": "int", "mandatory": true},
|
||||
{"name": "Interface", "type": "string", "mandatory": false}
|
||||
],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"UDPReceiver": {
|
||||
"fields": [
|
||||
{"name": "Port", "type": "int", "mandatory": true},
|
||||
{"name": "Address", "type": "string", "mandatory": false}
|
||||
],
|
||||
"direction": "IN"
|
||||
},
|
||||
"UDPSender": {
|
||||
"fields": [
|
||||
{"name": "Destination", "type": "string", "mandatory": true}
|
||||
],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"FileReader": {
|
||||
"fields": [
|
||||
{"name": "Filename", "type": "string", "mandatory": true},
|
||||
{"name": "Format", "type": "string", "mandatory": false},
|
||||
{"name": "Interpolate", "type": "string", "mandatory": false}
|
||||
],
|
||||
"direction": "IN"
|
||||
},
|
||||
"FileWriter": {
|
||||
"fields": [
|
||||
{"name": "Filename", "type": "string", "mandatory": true},
|
||||
{"name": "Format", "type": "string", "mandatory": false},
|
||||
{"name": "StoreOnTrigger", "type": "int", "mandatory": false}
|
||||
],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"OrderedClass": {
|
||||
"ordered": true,
|
||||
"fields": [
|
||||
{"name": "First", "type": "int", "mandatory": true},
|
||||
{"name": "Second", "type": "string", "mandatory": true}
|
||||
]
|
||||
},
|
||||
"BaseLib2GAM": { "fields": [] },
|
||||
"ConversionGAM": { "fields": [] },
|
||||
"DoubleHandshakeGAM": { "fields": [] },
|
||||
"FilterGAM": {
|
||||
"fields": [
|
||||
{"name": "Num", "type": "array", "mandatory": true},
|
||||
{"name": "Den", "type": "array", "mandatory": true},
|
||||
{"name": "ResetInEachState", "type": "any", "mandatory": false},
|
||||
{"name": "InputSignals", "type": "node", "mandatory": false},
|
||||
{"name": "OutputSignals", "type": "node", "mandatory": false}
|
||||
]
|
||||
},
|
||||
"HistogramGAM": {
|
||||
"fields": [
|
||||
{"name": "BeginCycleNumber", "type": "int", "mandatory": false},
|
||||
{"name": "StateChangeResetName", "type": "string", "mandatory": false},
|
||||
{"name": "InputSignals", "type": "node", "mandatory": false},
|
||||
{"name": "OutputSignals", "type": "node", "mandatory": false}
|
||||
]
|
||||
},
|
||||
"Interleaved2FlatGAM": { "fields": [] },
|
||||
"FlattenedStructIOGAM": { "fields": [] },
|
||||
"MathExpressionGAM": {
|
||||
"fields": [
|
||||
{"name": "Expression", "type": "string", "mandatory": true},
|
||||
{"name": "InputSignals", "type": "node", "mandatory": false},
|
||||
{"name": "OutputSignals", "type": "node", "mandatory": false}
|
||||
]
|
||||
},
|
||||
"MessageGAM": { "fields": [] },
|
||||
"MuxGAM": { "fields": [] },
|
||||
"SimulinkWrapperGAM": { "fields": [] },
|
||||
"SSMGAM": { "fields": [] },
|
||||
"StatisticsGAM": { "fields": [] },
|
||||
"TimeCorrectionGAM": { "fields": [] },
|
||||
"TriggeredIOGAM": { "fields": [] },
|
||||
"WaveformGAM": { "fields": [] },
|
||||
"DAN": { "fields": [], "direction": "OUT" },
|
||||
"LinuxTimer": {
|
||||
"fields": [
|
||||
{"name": "ExecutionMode", "type": "string", "mandatory": false},
|
||||
{"name": "SleepNature", "type": "string", "mandatory": false},
|
||||
{"name": "SleepPercentage", "type": "any", "mandatory": false},
|
||||
{"name": "Phase", "type": "int", "mandatory": false},
|
||||
{"name": "CPUMask", "type": "int", "mandatory": false},
|
||||
{"name": "TimeProvider", "type": "node", "mandatory": false},
|
||||
{"name": "Signals", "type": "node", "mandatory": true}
|
||||
],
|
||||
"direction": "IN"
|
||||
},
|
||||
"LinkDataSource": { "fields": [], "direction": "INOUT" },
|
||||
"MDSReader": {
|
||||
"fields": [
|
||||
{"name": "TreeName", "type": "string", "mandatory": true},
|
||||
{"name": "ShotNumber", "type": "int", "mandatory": true},
|
||||
{"name": "Frequency", "type": "float", "mandatory": true},
|
||||
{"name": "Signals", "type": "node", "mandatory": true}
|
||||
],
|
||||
"direction": "IN"
|
||||
},
|
||||
"MDSWriter": {
|
||||
"fields": [
|
||||
{"name": "NumberOfBuffers", "type": "int", "mandatory": true},
|
||||
{"name": "CPUMask", "type": "int", "mandatory": true},
|
||||
{"name": "StackSize", "type": "int", "mandatory": true},
|
||||
{"name": "TreeName", "type": "string", "mandatory": true},
|
||||
{"name": "PulseNumber", "type": "int", "mandatory": false},
|
||||
{"name": "StoreOnTrigger", "type": "int", "mandatory": true},
|
||||
{"name": "EventName", "type": "string", "mandatory": true},
|
||||
{"name": "TimeRefresh", "type": "float", "mandatory": true},
|
||||
{"name": "NumberOfPreTriggers", "type": "int", "mandatory": false},
|
||||
{"name": "NumberOfPostTriggers", "type": "int", "mandatory": false},
|
||||
{"name": "Signals", "type": "node", "mandatory": true},
|
||||
{"name": "Messages", "type": "node", "mandatory": false}
|
||||
],
|
||||
"direction": "OUT"
|
||||
},
|
||||
"NI1588TimeStamp": { "fields": [], "direction": "IN" },
|
||||
"NI6259ADC": { "fields": [], "direction": "IN" },
|
||||
"NI6259DAC": { "fields": [], "direction": "OUT" },
|
||||
"NI6259DIO": { "fields": [], "direction": "INOUT" },
|
||||
"NI6368ADC": { "fields": [], "direction": "IN" },
|
||||
"NI6368DAC": { "fields": [], "direction": "OUT" },
|
||||
"NI6368DIO": { "fields": [], "direction": "INOUT" },
|
||||
"NI9157CircularFifoReader": { "fields": [], "direction": "IN" },
|
||||
"NI9157MxiDataSource": { "fields": [], "direction": "INOUT" },
|
||||
"OPCUADSInput": { "fields": [], "direction": "IN" },
|
||||
"OPCUADSOutput": { "fields": [], "direction": "OUT" },
|
||||
"RealTimeThreadAsyncBridge": { "fields": [] },
|
||||
"RealTimeThreadSynchronisation": { "fields": [] },
|
||||
"UARTDataSource": { "fields": [], "direction": "INOUT" },
|
||||
"BaseLib2Wrapper": { "fields": [] },
|
||||
"EPICSCAClient": { "fields": [] },
|
||||
"EPICSPVA": { "fields": [] },
|
||||
"MemoryGate": { "fields": [] },
|
||||
"OPCUA": { "fields": [] },
|
||||
"SysLogger": { "fields": [] },
|
||||
"GAMDataSource": { "fields": [], "direction": "INOUT" }
|
||||
}
|
||||
}
|
||||
@@ -2,137 +2,73 @@ package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
)
|
||||
|
||||
//go:embed marte.json
|
||||
var defaultSchemaJSON []byte
|
||||
//go:embed marte.cue
|
||||
var defaultSchemaCUE []byte
|
||||
|
||||
type Schema struct {
|
||||
Classes map[string]ClassDefinition `json:"classes"`
|
||||
}
|
||||
|
||||
type ClassDefinition struct {
|
||||
Fields []FieldDefinition `json:"fields"`
|
||||
Ordered bool `json:"ordered"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type FieldDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "int", "float", "string", "bool", "reference", "array", "node", "any"
|
||||
Mandatory bool `json:"mandatory"`
|
||||
Context *cue.Context
|
||||
Value cue.Value
|
||||
}
|
||||
|
||||
func NewSchema() *Schema {
|
||||
ctx := cuecontext.New()
|
||||
return &Schema{
|
||||
Classes: make(map[string]ClassDefinition),
|
||||
Context: ctx,
|
||||
Value: ctx.CompileBytes(defaultSchemaCUE),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadSchema(path string) (*Schema, error) {
|
||||
// LoadSchema loads a CUE schema from a file and returns the cue.Value
|
||||
func LoadSchema(ctx *cue.Context, path string) (cue.Value, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var s Schema
|
||||
if err := json.Unmarshal(content, &s); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse schema: %v", err)
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// DefaultSchema returns the built-in embedded schema
|
||||
func DefaultSchema() *Schema {
|
||||
var s Schema
|
||||
if err := json.Unmarshal(defaultSchemaJSON, &s); err != nil {
|
||||
panic(fmt.Sprintf("failed to parse default embedded schema: %v", err))
|
||||
}
|
||||
if s.Classes == nil {
|
||||
s.Classes = make(map[string]ClassDefinition)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// Merge adds rules from 'other' to 's'.
|
||||
// Rules for the same class are merged (new fields added, existing fields updated).
|
||||
func (s *Schema) Merge(other *Schema) {
|
||||
if other == nil {
|
||||
return
|
||||
}
|
||||
for className, classDef := range other.Classes {
|
||||
if existingClass, ok := s.Classes[className]; ok {
|
||||
// Merge fields
|
||||
fieldMap := make(map[string]FieldDefinition)
|
||||
for _, f := range classDef.Fields {
|
||||
fieldMap[f.Name] = f
|
||||
}
|
||||
|
||||
var mergedFields []FieldDefinition
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// Keep existing fields, update if present in other
|
||||
for _, f := range existingClass.Fields {
|
||||
if newF, ok := fieldMap[f.Name]; ok {
|
||||
mergedFields = append(mergedFields, newF)
|
||||
} else {
|
||||
mergedFields = append(mergedFields, f)
|
||||
}
|
||||
seen[f.Name] = true
|
||||
}
|
||||
|
||||
// Append new fields
|
||||
for _, f := range classDef.Fields {
|
||||
if !seen[f.Name] {
|
||||
mergedFields = append(mergedFields, f)
|
||||
}
|
||||
}
|
||||
|
||||
existingClass.Fields = mergedFields
|
||||
if classDef.Ordered {
|
||||
existingClass.Ordered = true
|
||||
}
|
||||
if classDef.Direction != "" {
|
||||
existingClass.Direction = classDef.Direction
|
||||
}
|
||||
s.Classes[className] = existingClass
|
||||
} else {
|
||||
s.Classes[className] = classDef
|
||||
}
|
||||
return cue.Value{}, err
|
||||
}
|
||||
return ctx.CompileBytes(content), nil
|
||||
}
|
||||
|
||||
func LoadFullSchema(projectRoot string) *Schema {
|
||||
s := DefaultSchema()
|
||||
ctx := cuecontext.New()
|
||||
baseVal := ctx.CompileBytes(defaultSchemaCUE)
|
||||
if baseVal.Err() != nil {
|
||||
// Fallback or panic? Panic is appropriate for embedded schema failure
|
||||
panic(fmt.Sprintf("Embedded schema invalid: %v", baseVal.Err()))
|
||||
}
|
||||
|
||||
// 1. System Paths
|
||||
sysPaths := []string{
|
||||
"/usr/share/mdt/marte_schema.json",
|
||||
"/usr/share/mdt/marte_schema.cue",
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
sysPaths = append(sysPaths, filepath.Join(home, ".local/share/mdt/marte_schema.json"))
|
||||
sysPaths = append(sysPaths, filepath.Join(home, ".local/share/mdt/marte_schema.cue"))
|
||||
}
|
||||
|
||||
for _, path := range sysPaths {
|
||||
if sysSchema, err := LoadSchema(path); err == nil {
|
||||
s.Merge(sysSchema)
|
||||
if val, err := LoadSchema(ctx, path); err == nil && val.Err() == nil {
|
||||
baseVal = baseVal.Unify(val)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Project Path
|
||||
if projectRoot != "" {
|
||||
projectSchemaPath := filepath.Join(projectRoot, ".marte_schema.json")
|
||||
if projSchema, err := LoadSchema(projectSchemaPath); err == nil {
|
||||
s.Merge(projSchema)
|
||||
projectSchemaPath := filepath.Join(projectRoot, ".marte_schema.cue")
|
||||
if val, err := LoadSchema(ctx, projectSchemaPath); err == nil && val.Err() == nil {
|
||||
baseVal = baseVal.Unify(val)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
return &Schema{
|
||||
Context: ctx,
|
||||
Value: baseVal,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,15 +21,32 @@ The executable should support the following subcommands:
|
||||
The LSP server should provide the following capabilities:
|
||||
|
||||
- **Diagnostics**: Report syntax errors and validation issues.
|
||||
- **Incremental Sync**: Supports `textDocumentSync` kind 2 (Incremental) for better performance with large files.
|
||||
- **Hover Documentation**:
|
||||
- **Objects**: Display `CLASS::Name` and any associated docstrings.
|
||||
- **Signals**: Display `DataSource.Name TYPE (SIZE) [IN/OUT/INOUT]` along with docstrings.
|
||||
- **GAMs**: Show the list of States where the GAM is referenced.
|
||||
- **Referenced Signals**: Show the list of GAMs where the signal is referenced.
|
||||
- **Referenced Signals**: Show the list of GAMs where the signal is referenced (indicating Input/Output direction).
|
||||
- **Go to Definition**: Jump to the definition of a reference, supporting navigation across any file in the current project.
|
||||
- **Go to References**: Find usages of a node or field, supporting navigation across any file in the current project.
|
||||
- **Code Completion**: Autocomplete fields, values, and references.
|
||||
- **Code Snippets**: Provide snippets for common patterns.
|
||||
- **Context-Aware**: Suggestions depend on the cursor position (e.g., inside an object, assigning a value).
|
||||
- **Schema-Driven**: Field suggestions are derived from the CUE schema for the current object's Class, indicating mandatory vs. optional fields.
|
||||
- **Reference Suggestions**:
|
||||
- `DataSource` fields suggest available DataSource objects.
|
||||
- `Functions` (in Threads) suggest available GAM objects.
|
||||
- **Signal Completion**: Inside `InputSignals` or `OutputSignals` of a GAM:
|
||||
- Suggests available signals from valid DataSources (filtering by direction: `IN`/`INOUT` for Inputs, `OUT`/`INOUT` for Outputs).
|
||||
- Format: `SIGNAL_NAME:DATASOURCE_NAME`.
|
||||
- Auto-inserts: `SIGNAL_NAME = { DataSource = DATASOURCE_NAME }`.
|
||||
- **Rename Symbol**: Rename an object, field, or reference across the entire project scope.
|
||||
- Supports renaming of Definitions (`+Name` or `Name`), preserving any modifiers (`+`/`$`).
|
||||
- Updates all references to the renamed symbol, including qualified references (e.g., `Pkg.Name`).
|
||||
- **Inlay Hints**: Provide real-time contextual information inline.
|
||||
- **Signal Metadata**: Displays `::TYPE[ELEMENTSxDIMENSIONS]` next to signal names.
|
||||
- **Object Class**: Displays `CLASS::` before object references.
|
||||
- **Evaluation**: Displays results of expressions (` => RESULT`) and variable references (`(=> VALUE)`).
|
||||
- **Code Snippets**: Provide snippets for common patterns (e.g., `+Object = { ... }`).
|
||||
- **Formatting**: Format the document using the same rules and engine as the `fmt` command.
|
||||
|
||||
## Build System & File Structure
|
||||
@@ -45,11 +62,11 @@ The LSP server should provide the following capabilities:
|
||||
- **Build Process**:
|
||||
- The build tool merges all files sharing the same base namespace into a **single output configuration**.
|
||||
- **Namespace Consistency**: The build tool must verify that all input files belong to the same project namespace (the first segment of the `#package` URI). If multiple project namespaces are detected, the build must fail with an error.
|
||||
- **Target**: The build output is written to a single target file (e.g., provided via CLI or API).
|
||||
- **Target**: The build output is written to standard output (`stdout`) by default. It can be written to a target file if the `-o` (or `--output`) argument is provided via CLI.
|
||||
- **Multi-File Definitions**: Nodes and objects can be defined across multiple files. The build tool, validator, and LSP must merge these definitions (including all fields and sub-nodes) from the entire project to create a unified view before processing or validating.
|
||||
- **Global References**: References to nodes, signals, or objects can point to definitions located in any file within the project.
|
||||
- **Merging Order**: For objects defined across multiple files, the **first file** to be considered is the one containing the `Class` field definition.
|
||||
- **Field Order**: Within a single file, the relative order of defined fields must be maintained.
|
||||
- **Global References**: References to nodes, signals, or objects can point to definitions located in any file within the project. Support for dot-separated paths (e.g., `Node.SubNode`) is required.
|
||||
- **Merging Order**: For objects defined across multiple files, definitions are merged. The build tool must preserve the relative order of fields and sub-nodes as they appear in the source files, interleaving them correctly in the final output.
|
||||
- **Field Order**: Within a single file (and across merged files), the relative order of defined fields must be maintained in the output.
|
||||
- The LSP indexes only files belonging to the same project/namespace scope.
|
||||
- **Output**: The output format is the same as the input configuration but without the `#package` macro.
|
||||
|
||||
@@ -58,22 +75,31 @@ The LSP server should provide the following capabilities:
|
||||
### Grammar
|
||||
|
||||
- `comment` : `//.*`
|
||||
- `configuration`: `definition+`
|
||||
- `configuration`: `(definition | macro)+`
|
||||
- `definition`: `field = value | node = subnode`
|
||||
- `macro`: `package | variable | constant`
|
||||
- `field`: `[a-zA-Z][a-zA-Z0-9_\-]*`
|
||||
- `node`: `[+$][a-zA-Z][a-zA-Z0-9_\-]*`
|
||||
- `subnode`: `{ definition+ }`
|
||||
- `value`: `string|int|float|bool|reference|array`
|
||||
- `subnode`: `{ (definition | macro)+ }`
|
||||
- `value`: `expression`
|
||||
- `expression`: `atom | binary_expr | unary_expr`
|
||||
- `atom`: `string | int | float | bool | reference | array | "(" expression ")"`
|
||||
- `binary_expr`: `expression operator expression`
|
||||
- `unary_expr`: `unary_operator expression`
|
||||
- `operator`: `+ | - | * | / | % | & | | | ^ | ..`
|
||||
- `unary_operator`: `- | !`
|
||||
- `int`: `/-?[0-9]+|0b[01]+|0x[0-9a-fA-F]+`
|
||||
- `float`: `-?[0-9]+\.[0-9]+|-?[0-9]+\.?[0-9]*e\-?[0-9]+`
|
||||
- `float`: `-?[0-9]+\.[0-9]+|-?[0-9]+\.?[0-9]*[eE][+-]?[0-9]+`
|
||||
- `bool`: `true|false`
|
||||
- `string`: `".*"`
|
||||
- `reference` : `string|.*`
|
||||
- `array`: `{ value }`
|
||||
- `reference` : `[a-zA-Z][a-zA-Z0-9_\-\.]* | @[a-zA-Z0-9_]+ | $[a-zA-Z0-9_]+`
|
||||
- `array`: `{ (value | ",")* }`
|
||||
|
||||
#### Extended grammar
|
||||
|
||||
- `package` : `#package URI`
|
||||
- `variable`: `#var NAME: TYPE [= expression]`
|
||||
- `constant`: `#let NAME: TYPE = expression`
|
||||
- `URI`: `PROJECT | PROJECT.PRJ_SUB_URI`
|
||||
- `PRJ_SUB_URI`: `NODE | NODE.PRJ_SUB_URI`
|
||||
- `docstring` : `//#.*`
|
||||
@@ -84,13 +110,17 @@ The LSP server should provide the following capabilities:
|
||||
- **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object.
|
||||
- **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined).
|
||||
- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field.
|
||||
- **Variables (`#var`)**: Define overrideable parameters. Can be overridden via CLI (`-vVAR=VAL`).
|
||||
- **Constants (`#let`)**: Define fixed parameters. **Cannot** be overridden externally. Must have an initial value.
|
||||
- **Expressions**: Evaluated during build and displayed evaluated in LSP hover documentation.
|
||||
- **Docstrings (`//#`)**: Associated with the following definition (Node, Field, Variable, or Constant).
|
||||
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. Supported pragmas:
|
||||
- `//!unused: REASON` or `//!ignore(unused): REASON` - Suppress "Unused GAM" or "Unused Signal" warnings.
|
||||
- `//!implicit: REASON` or `//!ignore(implicit): REASON` - Suppress "Implicitly Defined Signal" warnings.
|
||||
- `//!allow(WARNING_TYPE): REASON` or `//!ignore(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`).
|
||||
- `//!allow(WARNING_TYPE): REASON` or `//!ignore(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`, `not_consumed`, `not_produced`).
|
||||
- `//!cast(DEF_TYPE, CUR_TYPE): REASON` - Suppress "Type Inconsistency" errors if types match.
|
||||
- **Structure**: A configuration is composed by one or more definitions.
|
||||
- **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition (Field, Node, or Object) is **not allowed** and must generate a parsing error.
|
||||
- **Structure**: A configuration is composed by one or more definitions or macros.
|
||||
- **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition/macro is **not allowed** and must generate a parsing error.
|
||||
|
||||
### Core MARTe Classes
|
||||
|
||||
@@ -111,6 +141,7 @@ MARTe configurations typically involve several main categories of objects:
|
||||
- All signal definitions **must** include a `Type` field with a valid value.
|
||||
- **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`.
|
||||
- **Property Matching**: Signal references in GAMs must match the properties (`Type`, `NumberOfElements`, `NumberOfDimensions`) of the defined signal in the `DataSource`.
|
||||
- **Consistency**: Implicit signals used across different GAMs must share the same `Type` and size properties.
|
||||
- **Extensibility**: Signal definitions can include additional fields as required by the specific application context.
|
||||
- **Signal Reference Syntax**:
|
||||
- Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats:
|
||||
@@ -132,6 +163,7 @@ MARTe configurations typically involve several main categories of objects:
|
||||
```
|
||||
In this case, `Alias` points to the DataSource signal name.
|
||||
- **Implicit Definition Constraint**: If a signal is implicitly defined within a GAM, the `Type` field **must** be present in the reference block to define the signal's properties.
|
||||
- **Renaming**: Renaming a signal (explicit or implicit) via LSP updates all its usages across all GAMs and DataSources in the project. Local aliases (`Alias = Name`) are preserved while their targets are updated.
|
||||
- **Directionality**: DataSources and their signals are directional:
|
||||
- `Input` (IN): Only providing data. Signals can only be used in `InputSignals`.
|
||||
- `Output` (OUT): Only receiving data. Signals can only be used in `OutputSignals`.
|
||||
@@ -142,9 +174,11 @@ MARTe configurations typically involve several main categories of objects:
|
||||
|
||||
The tool must build an index of the configuration to support LSP features and validations:
|
||||
|
||||
- **Recursive Indexing**: All `.marte` files in the project root and subdirectories are indexed automatically.
|
||||
- **GAMs**: Referenced in `$APPLICATION.States.$STATE_NAME.Threads.$THREAD_NAME.Functions` (where `$APPLICATION` is a `RealTimeApplication` node).
|
||||
- **Signals**: Referenced within the `InputSignals` and `OutputSignals` sub-nodes of a GAM.
|
||||
- **DataSources**: Referenced within the `DataSource` field of a signal reference/definition.
|
||||
- **Variables/Constants**: Referenced via `@NAME` or `$NAME` in expressions.
|
||||
- **General References**: Objects can also be referenced in other fields (e.g., as targets for messages).
|
||||
|
||||
### Validation Rules
|
||||
@@ -160,13 +194,14 @@ The tool must build an index of the configuration to support LSP features and va
|
||||
- **Field Order**: Verification that specific fields appear in a prescribed order when required by the class definition.
|
||||
- **Conditional Fields**: Validation of fields whose presence or value depends on the values of other fields within the same node or context.
|
||||
- **Schema Definition**:
|
||||
- Class validation rules must be defined in a separate schema file.
|
||||
- Class validation rules must be defined in a separate schema file using the **CUE** language.
|
||||
- **Metadata**: Class properties like direction (`#direction`) and multithreading support (`#multithreaded`) are stored within a `#meta` field in the class definition (e.g., `#meta: { direction: "IN", multithreaded: true }`).
|
||||
- **Project-Specific Classes**: Developers can define their own project-specific classes and corresponding validation rules, expanding the validation capabilities for their specific needs.
|
||||
- **Schema Loading**:
|
||||
- **Default Schema**: The tool should look for a default schema file `marte_schema.json` in standard system locations:
|
||||
- `/usr/share/mdt/marte_schema.json`
|
||||
- `$HOME/.local/share/mdt/marte_schema.json`
|
||||
- **Project Schema**: If a file named `.marte_schema.json` exists in the project root, it must be loaded.
|
||||
- **Default Schema**: The tool should look for a default schema file `marte_schema.cue` in standard system locations:
|
||||
- `/usr/share/mdt/marte_schema.cue`
|
||||
- `$HOME/.local/share/mdt/marte_schema.cue`
|
||||
- **Project Schema**: If a file named `.marte_schema.cue` exists in the project root, it must be loaded.
|
||||
- **Merging**: The final schema is a merge of the built-in schema, the system default schema (if found), and the project-specific schema. Rules in later sources (Project > System > Built-in) append to or override earlier ones.
|
||||
- **Duplicate Fields**:
|
||||
- **Constraint**: A field must not be defined more than once within the same object/node scope, even if those definitions are spread across different files.
|
||||
@@ -206,6 +241,7 @@ The LSP and `check` command should report the following:
|
||||
- Field type mismatches.
|
||||
- Grammar errors (e.g., missing closing brackets).
|
||||
- **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes.
|
||||
- **Threading Violation**: A DataSource that is not marked as multithreaded (via `#meta.multithreaded`) is used by GAMs running in different threads within the same State.
|
||||
|
||||
## Logging
|
||||
|
||||
|
||||
78
test/advanced_numbers_test.go
Normal file
78
test/advanced_numbers_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/formatter"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func TestAdvancedNumbers(t *testing.T) {
|
||||
content := `
|
||||
Hex = 0xFF
|
||||
HexLower = 0xee
|
||||
Binary = 0b1011
|
||||
Decimal = 123
|
||||
Scientific = 1e-3
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify values
|
||||
foundHex := false
|
||||
foundHexLower := false
|
||||
foundBinary := false
|
||||
for _, def := range cfg.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok {
|
||||
if f.Name == "Hex" {
|
||||
if v, ok := f.Value.(*parser.IntValue); ok {
|
||||
if v.Value != 255 {
|
||||
t.Errorf("Expected 255 for Hex, got %d", v.Value)
|
||||
}
|
||||
foundHex = true
|
||||
}
|
||||
}
|
||||
if f.Name == "HexLower" {
|
||||
if v, ok := f.Value.(*parser.IntValue); ok {
|
||||
if v.Value != 238 {
|
||||
t.Errorf("Expected 238 for HexLower, got %d", v.Value)
|
||||
}
|
||||
foundHexLower = true
|
||||
} else {
|
||||
t.Errorf("HexLower was parsed as %T, expected *parser.IntValue", f.Value)
|
||||
}
|
||||
}
|
||||
if f.Name == "Binary" {
|
||||
if v, ok := f.Value.(*parser.IntValue); ok {
|
||||
if v.Value == 11 {
|
||||
foundBinary = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundHex { t.Error("Hex field not found") }
|
||||
if !foundHexLower { t.Error("HexLower field not found") }
|
||||
if !foundBinary { t.Error("Binary field not found") }
|
||||
|
||||
// Verify formatting
|
||||
var buf bytes.Buffer
|
||||
formatter.Format(cfg, &buf)
|
||||
formatted := buf.String()
|
||||
if !contains(formatted, "Hex = 0xFF") {
|
||||
t.Errorf("Formatted content missing Hex = 0xFF:\n%s", formatted)
|
||||
}
|
||||
if !contains(formatted, "HexLower = 0xee") {
|
||||
t.Errorf("Formatted content missing HexLower = 0xee:\n%s", formatted)
|
||||
}
|
||||
if !contains(formatted, "Binary = 0b1011") {
|
||||
t.Errorf("Formatted content missing Binary = 0b1011:\n%s", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return bytes.Contains([]byte(s), []byte(substr))
|
||||
}
|
||||
109
test/ast_test.go
Normal file
109
test/ast_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestASTCoverage(t *testing.T) {
|
||||
pos := parser.Position{Line: 1, Column: 1}
|
||||
|
||||
var n parser.Node
|
||||
var d parser.Definition
|
||||
var v parser.Value
|
||||
|
||||
// Field
|
||||
f := &parser.Field{Position: pos}
|
||||
n = f
|
||||
d = f
|
||||
if n.Pos() != pos {
|
||||
t.Error("Field.Pos failed")
|
||||
}
|
||||
_ = d
|
||||
|
||||
// ObjectNode
|
||||
o := &parser.ObjectNode{Position: pos}
|
||||
n = o
|
||||
d = o
|
||||
if n.Pos() != pos {
|
||||
t.Error("ObjectNode.Pos failed")
|
||||
}
|
||||
|
||||
// StringValue
|
||||
sv := &parser.StringValue{Position: pos}
|
||||
n = sv
|
||||
v = sv
|
||||
if n.Pos() != pos {
|
||||
t.Error("StringValue.Pos failed")
|
||||
}
|
||||
_ = v
|
||||
|
||||
// IntValue
|
||||
iv := &parser.IntValue{Position: pos}
|
||||
n = iv
|
||||
v = iv
|
||||
if n.Pos() != pos {
|
||||
t.Error("IntValue.Pos failed")
|
||||
}
|
||||
|
||||
// FloatValue
|
||||
fv := &parser.FloatValue{Position: pos}
|
||||
n = fv
|
||||
v = fv
|
||||
if n.Pos() != pos {
|
||||
t.Error("FloatValue.Pos failed")
|
||||
}
|
||||
|
||||
// BoolValue
|
||||
bv := &parser.BoolValue{Position: pos}
|
||||
n = bv
|
||||
v = bv
|
||||
if n.Pos() != pos {
|
||||
t.Error("BoolValue.Pos failed")
|
||||
}
|
||||
|
||||
// ReferenceValue
|
||||
rv := &parser.ReferenceValue{Position: pos}
|
||||
n = rv
|
||||
v = rv
|
||||
if n.Pos() != pos {
|
||||
t.Error("ReferenceValue.Pos failed")
|
||||
}
|
||||
|
||||
// ArrayValue
|
||||
av := &parser.ArrayValue{Position: pos}
|
||||
n = av
|
||||
v = av
|
||||
if n.Pos() != pos {
|
||||
t.Error("ArrayValue.Pos failed")
|
||||
}
|
||||
|
||||
// Package
|
||||
pkg := &parser.Package{Position: pos}
|
||||
n = pkg
|
||||
if n.Pos() != pos {
|
||||
t.Error("Package.Pos failed")
|
||||
}
|
||||
|
||||
// Subnode
|
||||
sn := &parser.Subnode{Position: pos}
|
||||
n = sn
|
||||
if n.Pos() != pos {
|
||||
t.Error("Subnode.Pos failed")
|
||||
}
|
||||
|
||||
// Comment
|
||||
cmt := &parser.Comment{Position: pos}
|
||||
n = cmt
|
||||
if n.Pos() != pos {
|
||||
t.Error("Comment.Pos failed")
|
||||
}
|
||||
|
||||
// Pragma
|
||||
prg := &parser.Pragma{Position: pos}
|
||||
n = prg
|
||||
if n.Pos() != pos {
|
||||
t.Error("Pragma.Pos failed")
|
||||
}
|
||||
}
|
||||
56
test/builder_merge_test.go
Normal file
56
test/builder_merge_test.go
Normal file
@@ -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") }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/builder"
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
)
|
||||
|
||||
func TestMultiFileBuildMergeAndOrder(t *testing.T) {
|
||||
@@ -32,7 +32,7 @@ FieldB = 20
|
||||
os.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"})
|
||||
b := builder.NewBuilder([]string{"build_multi_test/f1.marte", "build_multi_test/f2.marte"}, nil)
|
||||
|
||||
// Prepare output file
|
||||
// Should be +MyObj.marte (normalized MyObj.marte) - Actually checking content
|
||||
|
||||
88
test/evaluated_signal_props_test.go
Normal file
88
test/evaluated_signal_props_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
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 TestEvaluatedSignalProperties(t *testing.T) {
|
||||
content := `
|
||||
#let N: uint32 = 10
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Filename = "test.bin"
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 NumberOfElements = @N }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = DS Type = uint32 NumberOfElements = 10 }
|
||||
}
|
||||
}
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tree := index.NewProjectTree()
|
||||
tree.AddFile("test.marte", cfg)
|
||||
tree.ResolveReferences()
|
||||
|
||||
v := validator.NewValidator(tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
// There should be no errors because @N evaluates to 10
|
||||
for _, d := range v.Diagnostics {
|
||||
if d.Level == validator.LevelError {
|
||||
t.Errorf("Unexpected error: %s", d.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// Test mismatch with expression
|
||||
contentErr := `
|
||||
#let N: uint32 = 10
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Filename = "test.bin"
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 NumberOfElements = @N + 5 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = DS Type = uint32 NumberOfElements = 10 }
|
||||
}
|
||||
}
|
||||
`
|
||||
p2 := parser.NewParser(contentErr)
|
||||
cfg2, _ := p2.Parse()
|
||||
tree2 := index.NewProjectTree()
|
||||
tree2.AddFile("test_err.marte", cfg2)
|
||||
tree2.ResolveReferences()
|
||||
|
||||
v2 := validator.NewValidator(tree2, ".")
|
||||
v2.ValidateProject()
|
||||
|
||||
found := false
|
||||
for _, d := range v2.Diagnostics {
|
||||
if strings.Contains(d.Message, "property 'NumberOfElements' mismatch") {
|
||||
found = true
|
||||
if !strings.Contains(d.Message, "defined '15'") {
|
||||
t.Errorf("Expected defined '15', got message: %s", d.Message)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected property mismatch error for @N + 5")
|
||||
}
|
||||
}
|
||||
60
test/expression_parsing_test.go
Normal file
60
test/expression_parsing_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
)
|
||||
|
||||
func TestExpressionParsing(t *testing.T) {
|
||||
content := `
|
||||
#var A: int = 10
|
||||
#var B: int = 2
|
||||
|
||||
+Obj = {
|
||||
// 1. Multiple variables
|
||||
Expr1 = @A + @B + @A
|
||||
|
||||
// 2. Brackets
|
||||
Expr2 = (@A + 2) * @B
|
||||
|
||||
// 3. No space operator (variable name strictness)
|
||||
Expr3 = @A-2
|
||||
}
|
||||
`
|
||||
f, _ := os.CreateTemp("", "expr_test.marte")
|
||||
f.WriteString(content)
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{f.Name()}, nil)
|
||||
|
||||
outF, _ := os.CreateTemp("", "out.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)
|
||||
|
||||
// Expr1: 10 + 2 + 10 = 22
|
||||
if !strings.Contains(outStr, "Expr1 = 22") {
|
||||
t.Errorf("Expr1 failed. Got:\n%s", outStr)
|
||||
}
|
||||
|
||||
// Expr2: (10 + 2) * 2 = 24
|
||||
if !strings.Contains(outStr, "Expr2 = 24") {
|
||||
t.Errorf("Expr2 failed. Got:\n%s", outStr)
|
||||
}
|
||||
|
||||
// Expr3: 10 - 2 = 8
|
||||
if !strings.Contains(outStr, "Expr3 = 8") {
|
||||
t.Errorf("Expr3 failed. Got:\n%s", outStr)
|
||||
}
|
||||
}
|
||||
39
test/expression_whitespace_test.go
Normal file
39
test/expression_whitespace_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
)
|
||||
|
||||
func TestExpressionWhitespace(t *testing.T) {
|
||||
content := `
|
||||
+Obj = {
|
||||
NoSpace = 2+2
|
||||
WithSpace = 2 + 2
|
||||
}
|
||||
`
|
||||
f, _ := os.CreateTemp("", "expr_ws.marte")
|
||||
f.WriteString(content)
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{f.Name()}, nil)
|
||||
|
||||
outF, _ := os.CreateTemp("", "out.marte")
|
||||
defer os.Remove(outF.Name())
|
||||
b.Build(outF)
|
||||
outF.Close()
|
||||
|
||||
outContent, _ := os.ReadFile(outF.Name())
|
||||
outStr := string(outContent)
|
||||
|
||||
if !strings.Contains(outStr, "NoSpace = 4") {
|
||||
t.Errorf("NoSpace failed. Got:\n%s", outStr)
|
||||
}
|
||||
if !strings.Contains(outStr, "WithSpace = 4") {
|
||||
t.Errorf("WithSpace failed. Got:\n%s", outStr)
|
||||
}
|
||||
}
|
||||
55
test/formatter_coverage_test.go
Normal file
55
test/formatter_coverage_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
44
test/formatter_variables_test.go
Normal file
44
test/formatter_variables_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
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 TestFormatterVariables(t *testing.T) {
|
||||
content := `
|
||||
#var MyInt: int = 10
|
||||
#var MyStr: string | "A" = "default"
|
||||
|
||||
+Obj = {
|
||||
Field1 = @MyInt
|
||||
Field2 = @MyStr
|
||||
}
|
||||
`
|
||||
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)
|
||||
|
||||
output := buf.String()
|
||||
|
||||
// Parser reconstructs type expression with spaces
|
||||
if !strings.Contains(output, "#var MyInt: int = 10") {
|
||||
t.Errorf("Variable MyInt formatted incorrectly. Got:\n%s", output)
|
||||
}
|
||||
// Note: parser adds space after each token in TypeExpr
|
||||
// string | "A" -> "string | \"A\""
|
||||
if !strings.Contains(output, "#var MyStr: string | \"A\" = \"default\"") {
|
||||
t.Errorf("Variable MyStr formatted incorrectly. Got:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Field1 = @MyInt") {
|
||||
t.Errorf("Variable reference @MyInt formatted incorrectly. Got:\n%s", output)
|
||||
}}
|
||||
58
test/index_cleanup_test.go
Normal file
58
test/index_cleanup_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestIndexCleanup(t *testing.T) {
|
||||
idx := index.NewProjectTree()
|
||||
file := "cleanup.marte"
|
||||
content := `
|
||||
#package Pkg
|
||||
+Node = { Class = Type }
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx.AddFile(file, cfg)
|
||||
|
||||
// Check node exists
|
||||
// Root -> Pkg -> Node
|
||||
pkgNode := idx.Root.Children["Pkg"]
|
||||
if pkgNode == nil {
|
||||
t.Fatal("Pkg node should exist")
|
||||
}
|
||||
if pkgNode.Children["Node"] == nil {
|
||||
t.Fatal("Node should exist")
|
||||
}
|
||||
|
||||
// Update file: remove +Node
|
||||
content2 := `
|
||||
#package Pkg
|
||||
// Removed node
|
||||
`
|
||||
p2 := parser.NewParser(content2)
|
||||
cfg2, _ := p2.Parse()
|
||||
idx.AddFile(file, cfg2)
|
||||
|
||||
// Check Node is gone
|
||||
pkgNode = idx.Root.Children["Pkg"]
|
||||
if pkgNode == nil {
|
||||
// Pkg should exist because of #package Pkg
|
||||
t.Fatal("Pkg node should exist after update")
|
||||
}
|
||||
if pkgNode.Children["Node"] != nil {
|
||||
t.Error("Node should be gone")
|
||||
}
|
||||
|
||||
// Test removing file completely
|
||||
idx.RemoveFile(file)
|
||||
if len(idx.Root.Children) != 0 {
|
||||
t.Errorf("Root should be empty after removing file, got %d children", len(idx.Root.Children))
|
||||
}
|
||||
}
|
||||
66
test/index_test.go
Normal file
66
test/index_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
)
|
||||
|
||||
func TestNodeMap(t *testing.T) {
|
||||
pt := index.NewProjectTree()
|
||||
root := pt.Root
|
||||
|
||||
// Create structure: +A -> +B -> +C
|
||||
nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root}
|
||||
root.Children["A"] = nodeA
|
||||
|
||||
nodeB := &index.ProjectNode{Name: "B", RealName: "+B", Children: make(map[string]*index.ProjectNode), Parent: nodeA}
|
||||
nodeA.Children["B"] = nodeB
|
||||
|
||||
nodeC := &index.ProjectNode{Name: "C", RealName: "+C", Children: make(map[string]*index.ProjectNode), Parent: nodeB}
|
||||
nodeB.Children["C"] = nodeC
|
||||
|
||||
// Rebuild Index
|
||||
pt.RebuildIndex()
|
||||
|
||||
// Find by Name
|
||||
found := pt.FindNode(root, "C", nil)
|
||||
if found != nodeC {
|
||||
t.Errorf("FindNode(C) failed. Got %v, want %v", found, nodeC)
|
||||
}
|
||||
|
||||
// Find by RealName
|
||||
found = pt.FindNode(root, "+C", nil)
|
||||
if found != nodeC {
|
||||
t.Errorf("FindNode(+C) failed. Got %v, want %v", found, nodeC)
|
||||
}
|
||||
|
||||
// Find by Path
|
||||
found = pt.FindNode(root, "A.B.C", nil)
|
||||
if found != nodeC {
|
||||
t.Errorf("FindNode(A.B.C) failed. Got %v, want %v", found, nodeC)
|
||||
}
|
||||
|
||||
// Find by Path with RealName
|
||||
found = pt.FindNode(root, "+A.+B.+C", nil)
|
||||
if found != nodeC {
|
||||
t.Errorf("FindNode(+A.+B.+C) failed. Got %v, want %v", found, nodeC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReferencesWithMap(t *testing.T) {
|
||||
pt := index.NewProjectTree()
|
||||
root := pt.Root
|
||||
|
||||
nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root}
|
||||
root.Children["A"] = nodeA
|
||||
|
||||
ref := index.Reference{Name: "A", File: "test.marte"}
|
||||
pt.References = append(pt.References, ref)
|
||||
|
||||
pt.ResolveReferences()
|
||||
|
||||
if pt.References[0].Target != nodeA {
|
||||
t.Error("ResolveReferences failed to resolve A")
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/builder"
|
||||
"github.com/marte-dev/marte-dev-tools/internal/formatter"
|
||||
"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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
"github.com/marte-community/marte-dev-tools/internal/formatter"
|
||||
"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 TestCheckCommand(t *testing.T) {
|
||||
@@ -168,7 +168,7 @@ func TestBuildCommand(t *testing.T) {
|
||||
|
||||
// Test Merge
|
||||
files := []string{"integration/build_merge_1.marte", "integration/build_merge_2.marte"}
|
||||
b := builder.NewBuilder(files)
|
||||
b := builder.NewBuilder(files, nil)
|
||||
|
||||
outputFile, err := os.Create("build_test/TEST.marte")
|
||||
if err != nil {
|
||||
@@ -195,7 +195,7 @@ func TestBuildCommand(t *testing.T) {
|
||||
|
||||
// Test Order (Class First)
|
||||
filesOrder := []string{"integration/build_order_1.marte", "integration/build_order_2.marte"}
|
||||
bOrder := builder.NewBuilder(filesOrder)
|
||||
bOrder := builder.NewBuilder(filesOrder, nil)
|
||||
|
||||
outputFileOrder, err := os.Create("build_test/ORDER.marte")
|
||||
if err != nil {
|
||||
|
||||
38
test/isolation_test.go
Normal file
38
test/isolation_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestIsolatedFileIsolation(t *testing.T) {
|
||||
pt := index.NewProjectTree()
|
||||
|
||||
// File 1: Project file
|
||||
f1 := "#package P\n+A = { Class = C }"
|
||||
p1 := parser.NewParser(f1)
|
||||
c1, _ := p1.Parse()
|
||||
pt.AddFile("f1.marte", c1)
|
||||
|
||||
// File 2: Isolated file
|
||||
f2 := "+B = { Class = C }"
|
||||
p2 := parser.NewParser(f2)
|
||||
c2, _ := p2.Parse()
|
||||
pt.AddFile("f2.marte", c2)
|
||||
|
||||
pt.ResolveReferences()
|
||||
|
||||
// Try finding A from f2
|
||||
isoNode := pt.IsolatedFiles["f2.marte"]
|
||||
if pt.ResolveName(isoNode, "A", nil) != nil {
|
||||
t.Error("Isolated file f2 should not see global A")
|
||||
}
|
||||
|
||||
// Try finding B from f1
|
||||
pNode := pt.Root.Children["P"]
|
||||
if pt.ResolveName(pNode, "B", nil) != nil {
|
||||
t.Error("Project file f1 should not see isolated B")
|
||||
}
|
||||
}
|
||||
125
test/let_macro_test.go
Normal file
125
test/let_macro_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
"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 TestLetMacroFull(t *testing.T) {
|
||||
content := `
|
||||
//# My documentation
|
||||
#let MyConst: uint32 = 10 + 20
|
||||
+Obj = {
|
||||
Value = @MyConst
|
||||
}
|
||||
`
|
||||
tmpFile, _ := os.CreateTemp("", "let_*.marte")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
os.WriteFile(tmpFile.Name(), []byte(content), 0644)
|
||||
|
||||
// 1. Test Parsing & Indexing
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
tree := index.NewProjectTree()
|
||||
tree.AddFile(tmpFile.Name(), cfg)
|
||||
|
||||
vars := tree.Root.Variables
|
||||
if iso, ok := tree.IsolatedFiles[tmpFile.Name()]; ok {
|
||||
vars = iso.Variables
|
||||
}
|
||||
|
||||
info, ok := vars["MyConst"]
|
||||
if !ok || !info.Def.IsConst {
|
||||
t.Fatal("#let variable not indexed correctly as Const")
|
||||
}
|
||||
if info.Doc != "My documentation" {
|
||||
t.Errorf("Expected doc 'My documentation', got '%s'", info.Doc)
|
||||
}
|
||||
|
||||
// 2. Test Builder Evaluation
|
||||
out, _ := os.CreateTemp("", "let_out.cfg")
|
||||
defer os.Remove(out.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{tmpFile.Name()}, nil)
|
||||
if err := b.Build(out); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
outContent, _ := os.ReadFile(out.Name())
|
||||
if !strings.Contains(string(outContent), "Value = 30") {
|
||||
t.Errorf("Expected Value = 30 (evaluated @MyConst), got:\n%s", string(outContent))
|
||||
}
|
||||
|
||||
// 3. Test Override Protection
|
||||
out2, _ := os.CreateTemp("", "let_out2.cfg")
|
||||
defer os.Remove(out2.Name())
|
||||
|
||||
b2 := builder.NewBuilder([]string{tmpFile.Name()}, map[string]string{"MyConst": "100"})
|
||||
if err := b2.Build(out2); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
outContent2, _ := os.ReadFile(out2.Name())
|
||||
if !strings.Contains(string(outContent2), "Value = 30") {
|
||||
t.Errorf("Constant was overridden! Expected 30, got:\n%s", string(outContent2))
|
||||
}
|
||||
|
||||
// 4. Test Validator (Mandatory Value)
|
||||
contentErr := "#let BadConst: uint32"
|
||||
p2 := parser.NewParser(contentErr)
|
||||
cfg2, err2 := p2.Parse()
|
||||
// Parser might fail if = is missing?
|
||||
// parseLet expects =.
|
||||
if err2 == nil {
|
||||
// If parser didn't fail (maybe it was partial), validator should catch it
|
||||
tree2 := index.NewProjectTree()
|
||||
tree2.AddFile("err.marte", cfg2)
|
||||
v := validator.NewValidator(tree2, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "must have an initial value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && cfg2 != nil {
|
||||
// If p2.Parse() failed and added error to p2.errors, it's also fine.
|
||||
// But check if it reached validator.
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Test Duplicate Detection
|
||||
contentDup := `
|
||||
#let MyConst: uint32 = 10
|
||||
#var MyConst: uint32 = 20
|
||||
`
|
||||
p3 := parser.NewParser(contentDup)
|
||||
cfg3, _ := p3.Parse()
|
||||
tree3 := index.NewProjectTree()
|
||||
tree3.AddFile("dup.marte", cfg3)
|
||||
v3 := validator.NewValidator(tree3, ".")
|
||||
v3.ValidateProject()
|
||||
|
||||
foundDup := false
|
||||
for _, d := range v3.Diagnostics {
|
||||
if strings.Contains(d.Message, "Duplicate variable definition") {
|
||||
foundDup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundDup {
|
||||
t.Error("Expected duplicate variable definition error")
|
||||
}
|
||||
}
|
||||
45
test/lexer_coverage_test.go
Normal file
45
test/lexer_coverage_test.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
62
test/logger_test.go
Normal file
62
test/logger_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/logger"
|
||||
)
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestLoggerPrint")
|
||||
cmd.Env = append(os.Environ(), "TEST_LOGGER_PRINT=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("process failed: %v", err)
|
||||
}
|
||||
output := string(out)
|
||||
if !strings.Contains(output, "Test Printf 123") {
|
||||
t.Error("Printf output missing")
|
||||
}
|
||||
if !strings.Contains(output, "Test Println") {
|
||||
t.Error("Println output missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerFatal(t *testing.T) {
|
||||
if os.Getenv("TEST_LOGGER_FATAL") == "1" {
|
||||
logger.Fatal("Test Fatal")
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestLoggerFatal")
|
||||
cmd.Env = append(os.Environ(), "TEST_LOGGER_FATAL=1")
|
||||
err := cmd.Run()
|
||||
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
|
||||
return // Success (exit code non-zero)
|
||||
}
|
||||
t.Fatalf("process ran with err %v, want exit status 1", err)
|
||||
}
|
||||
|
||||
func TestLoggerFatalf(t *testing.T) {
|
||||
if os.Getenv("TEST_LOGGER_FATALF") == "1" {
|
||||
logger.Fatalf("Test Fatalf %d", 456)
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestLoggerFatalf")
|
||||
cmd.Env = append(os.Environ(), "TEST_LOGGER_FATALF=1")
|
||||
err := cmd.Run()
|
||||
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
|
||||
return // Success
|
||||
}
|
||||
t.Fatalf("process ran with err %v, want exit status 1", err)
|
||||
}
|
||||
85
test/lsp_app_test_repro_test.go
Normal file
85
test/lsp_app_test_repro_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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 TestLSPAppTestRepro(t *testing.T) {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".")
|
||||
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := `+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+TimingDataSource = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+FnA = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
A = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
Value = @Value
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
B = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State = {
|
||||
Class = RealTimeState
|
||||
Threads = {
|
||||
+Th1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = { FnA }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = TimingDataSource
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://examples/app_test.marte"
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
|
||||
})
|
||||
|
||||
output := buf.String()
|
||||
|
||||
// Check Unresolved Variable
|
||||
if !strings.Contains(output, "Unresolved variable reference: '@Value'") {
|
||||
t.Error("LSP missing unresolved variable error")
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
t.Log(output)
|
||||
}
|
||||
}
|
||||
90
test/lsp_completion_signals_robustness_test.go
Normal file
90
test/lsp_completion_signals_robustness_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/schema"
|
||||
)
|
||||
|
||||
func TestSuggestSignalsRobustness(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.ProjectRoot = "."
|
||||
lsp.GlobalSchema = schema.NewSchema()
|
||||
|
||||
// Inject schema with INOUT
|
||||
custom := []byte(`
|
||||
package schema
|
||||
#Classes: {
|
||||
InOutReader: { #meta: direction: "INOUT" }
|
||||
}
|
||||
`)
|
||||
val := lsp.GlobalSchema.Context.CompileBytes(custom)
|
||||
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
|
||||
|
||||
content := `
|
||||
+DS = {
|
||||
Class = InOutReader
|
||||
+Signals = {
|
||||
Sig = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
|
||||
}
|
||||
+OutputSignals = {
|
||||
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://robust.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("robust.marte", cfg)
|
||||
|
||||
// Check Input (Line 10)
|
||||
paramsIn := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 10, Character: 8},
|
||||
}
|
||||
listIn := lsp.HandleCompletion(paramsIn)
|
||||
found := false
|
||||
if listIn != nil {
|
||||
for _, item := range listIn.Items {
|
||||
if item.Label == "DS:Sig" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("INOUT signal not found in InputSignals")
|
||||
}
|
||||
|
||||
// Check Output (Line 13)
|
||||
paramsOut := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 13, Character: 8},
|
||||
}
|
||||
listOut := lsp.HandleCompletion(paramsOut)
|
||||
found = false
|
||||
if listOut != nil {
|
||||
for _, item := range listOut.Items {
|
||||
if item.Label == "DS:Sig" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("INOUT signal not found in OutputSignals")
|
||||
}
|
||||
}
|
||||
128
test/lsp_completion_signals_test.go
Normal file
128
test/lsp_completion_signals_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package integration
|
||||
|
||||
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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/schema"
|
||||
)
|
||||
|
||||
func TestSuggestSignalsInGAM(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.ProjectRoot = "."
|
||||
lsp.GlobalSchema = schema.NewSchema()
|
||||
|
||||
// Inject schema for directionality
|
||||
custom := []byte(`
|
||||
package schema
|
||||
#Classes: {
|
||||
FileReader: { direction: "IN" }
|
||||
FileWriter: { direction: "OUT" }
|
||||
}
|
||||
`)
|
||||
val := lsp.GlobalSchema.Context.CompileBytes(custom)
|
||||
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
|
||||
|
||||
content := `
|
||||
+InDS = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
InSig = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+OutDS = {
|
||||
Class = FileWriter
|
||||
+Signals = {
|
||||
OutSig = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
|
||||
}
|
||||
+OutputSignals = {
|
||||
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://signals.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("signals.marte", cfg)
|
||||
|
||||
// 1. Suggest in InputSignals
|
||||
// Line 16 (empty line inside InputSignals)
|
||||
paramsIn := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 16, Character: 8},
|
||||
}
|
||||
|
||||
listIn := lsp.HandleCompletion(paramsIn)
|
||||
if listIn == nil {
|
||||
t.Fatal("Expected suggestions in InputSignals")
|
||||
}
|
||||
|
||||
foundIn := false
|
||||
foundOut := false
|
||||
for _, item := range listIn.Items {
|
||||
if item.Label == "InDS:InSig" {
|
||||
foundIn = true
|
||||
// Normalize spaces for check
|
||||
insert := strings.ReplaceAll(item.InsertText, " ", "")
|
||||
expected := "InSig={DataSource=InDS}"
|
||||
if !strings.Contains(insert, expected) && !strings.Contains(item.InsertText, "InSig = {") {
|
||||
// Snippet might differ slightly, but should contain essentials
|
||||
t.Errorf("InsertText mismatch: %s", item.InsertText)
|
||||
}
|
||||
}
|
||||
if item.Label == "OutDS:OutSig" {
|
||||
foundOut = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundIn {
|
||||
t.Error("Did not find InDS:InSig")
|
||||
}
|
||||
if foundOut {
|
||||
t.Error("Should not find OutDS:OutSig in InputSignals")
|
||||
}
|
||||
|
||||
// 2. Suggest in OutputSignals
|
||||
// Line 19
|
||||
paramsOut := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 19, Character: 8},
|
||||
}
|
||||
listOut := lsp.HandleCompletion(paramsOut)
|
||||
if listOut == nil {
|
||||
t.Fatal("Expected suggestions in OutputSignals")
|
||||
}
|
||||
|
||||
foundIn = false
|
||||
foundOut = false
|
||||
for _, item := range listOut.Items {
|
||||
if item.Label == "InDS:InSig" {
|
||||
foundIn = true
|
||||
}
|
||||
if item.Label == "OutDS:OutSig" {
|
||||
foundOut = true
|
||||
}
|
||||
}
|
||||
|
||||
if foundIn {
|
||||
t.Error("Should not find InDS:InSig in OutputSignals")
|
||||
}
|
||||
if !foundOut {
|
||||
t.Error("Did not find OutDS:OutSig in OutputSignals")
|
||||
}
|
||||
}
|
||||
382
test/lsp_completion_test.go
Normal file
382
test/lsp_completion_test.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package integration
|
||||
|
||||
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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/schema"
|
||||
)
|
||||
|
||||
func TestHandleCompletion(t *testing.T) {
|
||||
setup := func() {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.ProjectRoot = "."
|
||||
lsp.GlobalSchema = schema.NewSchema()
|
||||
}
|
||||
|
||||
uri := "file://test.marte"
|
||||
path := "test.marte"
|
||||
|
||||
t.Run("Suggest Classes", func(t *testing.T) {
|
||||
setup()
|
||||
content := "+Obj = { Class = "
|
||||
lsp.Documents[uri] = content
|
||||
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 0, Character: len(content)},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil || len(list.Items) == 0 {
|
||||
t.Fatal("Expected class suggestions, got none")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "RealTimeApplication" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected RealTimeApplication in class suggestions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest Fields", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
+MyApp = {
|
||||
Class = RealTimeApplication
|
||||
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
// Position at line 3 (empty line inside MyApp)
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 4},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil || len(list.Items) == 0 {
|
||||
t.Fatal("Expected field suggestions, got none")
|
||||
}
|
||||
|
||||
foundData := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "Data" {
|
||||
foundData = true
|
||||
if item.Detail != "Mandatory" {
|
||||
t.Errorf("Expected Data to be Mandatory, got %s", item.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundData {
|
||||
t.Error("Expected 'Data' in field suggestions for RealTimeApplication")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest References (DataSource)", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
$App = {
|
||||
$Data = {
|
||||
+InDS = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
S1 = { DataSource = }
|
||||
}
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Position at end of "DataSource = "
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 14, Character: 28},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil || len(list.Items) == 0 {
|
||||
t.Fatal("Expected DataSource suggestions, got none")
|
||||
}
|
||||
|
||||
foundDS := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "InDS" {
|
||||
foundDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundDS {
|
||||
t.Error("Expected 'InDS' in suggestions for DataSource field")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Filter Existing Fields", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
+MyThread = {
|
||||
Class = RealTimeThread
|
||||
Functions = { }
|
||||
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
// Position at line 4
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 4, Character: 4},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "Functions" || item.Label == "Class" {
|
||||
t.Errorf("Did not expect already defined field %s in suggestions", item.Label)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Scope-aware suggestions", func(t *testing.T) {
|
||||
setup()
|
||||
// Define a project DataSource in one file
|
||||
cfg1, _ := parser.NewParser("#package MYPROJ.Data\n+ProjectDS = { Class = FileReader +Signals = { S1 = { Type = int32 } } }").Parse()
|
||||
lsp.Tree.AddFile("project_ds.marte", cfg1)
|
||||
|
||||
// Define an isolated file
|
||||
contentIso := "+MyGAM = { Class = IOGAM +InputSignals = { S1 = { DataSource = } } }"
|
||||
lsp.Documents["file://iso.marte"] = contentIso
|
||||
cfg2, _ := parser.NewParser(contentIso).Parse()
|
||||
lsp.Tree.AddFile("iso.marte", cfg2)
|
||||
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Completion in isolated file
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://iso.marte"},
|
||||
Position: lsp.Position{Line: 0, Character: strings.Index(contentIso, "DataSource = ") + len("DataSource = ") + 1},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
foundProjectDS := false
|
||||
if list != nil {
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "ProjectDS" {
|
||||
foundProjectDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if foundProjectDS {
|
||||
t.Error("Did not expect ProjectDS in isolated file suggestions (isolation)")
|
||||
}
|
||||
|
||||
// Completion in a project file
|
||||
lineContent := "+MyGAM = { Class = IOGAM +InputSignals = { S1 = { DataSource = Dummy } } }"
|
||||
contentPrj := "#package MYPROJ.App\n" + lineContent
|
||||
lsp.Documents["file://prj.marte"] = contentPrj
|
||||
pPrj := parser.NewParser(contentPrj)
|
||||
cfg3, err := pPrj.Parse()
|
||||
if err != nil {
|
||||
t.Logf("Parser error in contentPrj: %v", err)
|
||||
}
|
||||
lsp.Tree.AddFile("prj.marte", cfg3)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
paramsPrj := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://prj.marte"},
|
||||
Position: lsp.Position{Line: 1, Character: strings.Index(lineContent, "Dummy")},
|
||||
}
|
||||
|
||||
listPrj := lsp.HandleCompletion(paramsPrj)
|
||||
foundProjectDS = false
|
||||
if listPrj != nil {
|
||||
for _, item := range listPrj.Items {
|
||||
if item.Label == "ProjectDS" {
|
||||
foundProjectDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundProjectDS {
|
||||
t.Error("Expected ProjectDS in project file suggestions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest Signal Types", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Signals = {
|
||||
S1 = { Type = }
|
||||
}
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 4, Character: strings.Index(content, "Type = ") + len("Type = ") + 1},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil {
|
||||
t.Fatal("Expected signal type suggestions")
|
||||
}
|
||||
|
||||
foundUint32 := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "uint32" {
|
||||
foundUint32 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundUint32 {
|
||||
t.Error("Expected uint32 in suggestions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest CUE Enums", func(t *testing.T) {
|
||||
setup()
|
||||
// Inject custom schema with enum
|
||||
custom := []byte(`
|
||||
package schema
|
||||
#Classes: {
|
||||
TestEnumClass: {
|
||||
Mode: "Auto" | "Manual"
|
||||
}
|
||||
}
|
||||
`)
|
||||
val := lsp.GlobalSchema.Context.CompileBytes(custom)
|
||||
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
|
||||
|
||||
content := `
|
||||
+Obj = {
|
||||
Class = TestEnumClass
|
||||
Mode =
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: strings.Index(content, "Mode = ") + len("Mode = ") + 1},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil {
|
||||
t.Fatal("Expected enum suggestions")
|
||||
}
|
||||
|
||||
foundAuto := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "\"Auto\"" { // CUE string value includes quotes
|
||||
foundAuto = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundAuto {
|
||||
// Check if it returned without quotes?
|
||||
// v.String() returns quoted for string.
|
||||
t.Error("Expected \"Auto\" in suggestions")
|
||||
for _, item := range list.Items {
|
||||
t.Logf("Suggestion: %s", item.Label)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest Variables", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
#var MyVar: uint = 10
|
||||
+App = {
|
||||
Field =
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
// 1. Triggered by =
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 12}, // After "Field = "
|
||||
}
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil {
|
||||
t.Fatal("Expected suggestions")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "@MyVar" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected @MyVar in suggestions for =")
|
||||
}
|
||||
|
||||
// 2. Triggered by @
|
||||
// "Field = @"
|
||||
lsp.Documents[uri] = `
|
||||
#var MyVar: uint = 10
|
||||
+App = {
|
||||
Field = @
|
||||
}
|
||||
`
|
||||
params2 := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 13}, // After "Field = $"
|
||||
}
|
||||
list2 := lsp.HandleCompletion(params2)
|
||||
if list2 == nil {
|
||||
t.Fatal("Expected suggestions for @")
|
||||
}
|
||||
found = false
|
||||
for _, item := range list2.Items {
|
||||
if item.Label == "MyVar" { // suggestVariables returns "MyVar"
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected MyVar in suggestions for @")
|
||||
}
|
||||
})
|
||||
}
|
||||
191
test/lsp_coverage_test.go
Normal file
191
test/lsp_coverage_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"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) {
|
||||
lsp.Documents = make(map[string]string)
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := "Line1\nLine2\nLine3"
|
||||
uri := "file://inc.marte"
|
||||
lsp.Documents[uri] = content
|
||||
|
||||
// Replace "Line2" (Line 1, 0-5) with "Modified"
|
||||
change := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{
|
||||
Start: lsp.Position{Line: 1, Character: 0},
|
||||
End: lsp.Position{Line: 1, Character: 5},
|
||||
},
|
||||
Text: "Modified",
|
||||
}
|
||||
|
||||
params := lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: 2},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change},
|
||||
}
|
||||
|
||||
lsp.HandleDidChange(params)
|
||||
|
||||
expected := "Line1\nModified\nLine3"
|
||||
if lsp.Documents[uri] != expected {
|
||||
t.Errorf("Incremental update failed. Got:\n%q\nWant:\n%q", lsp.Documents[uri], expected)
|
||||
}
|
||||
|
||||
// Insert at end
|
||||
change2 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{
|
||||
Start: lsp.Position{Line: 2, Character: 5},
|
||||
End: lsp.Position{Line: 2, Character: 5},
|
||||
},
|
||||
Text: "\nLine4",
|
||||
}
|
||||
params2 := lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: 3},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change2},
|
||||
}
|
||||
lsp.HandleDidChange(params2)
|
||||
|
||||
expected2 := "Line1\nModified\nLine3\nLine4"
|
||||
if lsp.Documents[uri] != expected2 {
|
||||
t.Errorf("Incremental insert failed. Got:\n%q\nWant:\n%q", lsp.Documents[uri], expected2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLSPLifecycle(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
// Shutdown
|
||||
msgShutdown := &lsp.JsonRpcMessage{
|
||||
Method: "shutdown",
|
||||
ID: 1,
|
||||
}
|
||||
lsp.HandleMessage(msgShutdown)
|
||||
|
||||
if !strings.Contains(buf.String(), `"result":null`) {
|
||||
t.Error("Shutdown response incorrect")
|
||||
}
|
||||
|
||||
// Exit
|
||||
if os.Getenv("TEST_LSP_EXIT") == "1" {
|
||||
msgExit := &lsp.JsonRpcMessage{Method: "exit"}
|
||||
lsp.HandleMessage(msgExit)
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestLSPLifecycle")
|
||||
cmd.Env = append(os.Environ(), "TEST_LSP_EXIT=1")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
t.Errorf("Exit failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLSPMalformedParams(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
// Malformed Hover
|
||||
msg := &lsp.JsonRpcMessage{
|
||||
Method: "textDocument/hover",
|
||||
ID: 2,
|
||||
Params: json.RawMessage(`{invalid`),
|
||||
}
|
||||
lsp.HandleMessage(msg)
|
||||
|
||||
output := buf.String()
|
||||
// Should respond with nil result
|
||||
if !strings.Contains(output, `"result":null`) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
74
test/lsp_crash_test.go
Normal file
74
test/lsp_crash_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package integration
|
||||
|
||||
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 TestLSPCrashOnUndefinedReference(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+State = {
|
||||
Class = RealTimeState
|
||||
+Thread = {
|
||||
Class = RealTimeThread
|
||||
Functions = { UndefinedGAM }
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://crash.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("crash.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Line 7: " Functions = { UndefinedGAM }"
|
||||
// 12 spaces + "Functions" (9) + " = { " (5) = 26 chars prefix.
|
||||
// UndefinedGAM starts at 26.
|
||||
params := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 7, Character: 27},
|
||||
}
|
||||
|
||||
// This should NOT panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
res := lsp.HandleDefinition(params)
|
||||
|
||||
if res != nil {
|
||||
t.Error("Expected nil for undefined reference definition")
|
||||
}
|
||||
|
||||
// 2. Hover
|
||||
hParams := lsp.HoverParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 7, Character: 27},
|
||||
}
|
||||
hover := lsp.HandleHover(hParams)
|
||||
if hover == nil {
|
||||
t.Error("Expected hover for unresolved reference")
|
||||
} else {
|
||||
content := hover.Contents.(lsp.MarkupContent).Value
|
||||
if !strings.Contains(content, "Unresolved") {
|
||||
t.Errorf("Expected 'Unresolved' in hover, got: %s", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
155
test/lsp_diagnostics_app_test.go
Normal file
155
test/lsp_diagnostics_app_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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 TestLSPDiagnosticsAppTest(t *testing.T) {
|
||||
// Setup LSP environment
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".") // Use default schema
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
// Content from examples/app_test.marte (implicit signals, unresolved var, ordering error)
|
||||
content := `+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+TimingDataSource = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+FnA = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
A = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
Value = @Value
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
B = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State = {
|
||||
Class = RealTimeState
|
||||
Threads = {
|
||||
+Th1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = { FnA }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = TimingDataSource
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://app_test.marte"
|
||||
|
||||
// Simulate DidOpen
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{
|
||||
URI: uri,
|
||||
Text: content,
|
||||
},
|
||||
})
|
||||
|
||||
output := buf.String()
|
||||
|
||||
// Verify Diagnostics are published
|
||||
if !strings.Contains(output, "textDocument/publishDiagnostics") {
|
||||
t.Fatal("LSP did not publish diagnostics")
|
||||
}
|
||||
|
||||
// 1. Check Unresolved Variable Error (@Value)
|
||||
if !strings.Contains(output, "Unresolved variable reference: '@Value'") {
|
||||
t.Error("Missing diagnostic for unresolved variable '@Value'")
|
||||
}
|
||||
|
||||
// 2. Check INOUT Unused Warning (Signal B produced but not consumed)
|
||||
// Message format: INOUT Signal 'B' ... produced ... but never consumed ...
|
||||
if !strings.Contains(output, "INOUT Signal 'B'") || !strings.Contains(output, "never consumed") {
|
||||
t.Error("Missing diagnostic for unused INOUT signal (Signal B)")
|
||||
}
|
||||
|
||||
// 4. Check Implicit Signal Warnings (A and B)
|
||||
if !strings.Contains(output, "Implicitly Defined Signal: 'A'") {
|
||||
t.Error("Missing diagnostic for implicit signal 'A'")
|
||||
}
|
||||
if !strings.Contains(output, "Implicitly Defined Signal: 'B'") {
|
||||
t.Error("Missing diagnostic for implicit signal 'B'")
|
||||
}
|
||||
|
||||
// Check Unused GAM Warning (FnA is used in Th1, so should NOT be unused)
|
||||
// Wait, is FnA used?
|
||||
// Functions = { FnA }.
|
||||
// resolveScopedName should find it?
|
||||
// In previous analysis, FnA inside Functions container might be hard to find from State?
|
||||
// But TestLSPAppTestRepro passed?
|
||||
// If FindNode finds it (Validator uses FindNode), then it is referenced.
|
||||
// CheckUnused uses `v.Tree.References`.
|
||||
// `ResolveReferences` populates references.
|
||||
// `ResolveReferences` uses `resolveScopedName`.
|
||||
// If `resolveScopedName` fails to find FnA from Th1 (because FnA is in Functions and not sibling/ancestor),
|
||||
// Then `ref.Target` is nil.
|
||||
// So `FnA` is NOT referenced in Index.
|
||||
// So `CheckUnused` reports "Unused GAM".
|
||||
|
||||
// BUT Validator uses `resolveReference` (FindNode) to verify Functions array.
|
||||
// So Validator knows it is valid.
|
||||
// But `CheckUnused` relies on Index References.
|
||||
|
||||
// If Index doesn't resolve it, `CheckUnused` warns.
|
||||
// Does output contain "Unused GAM: +FnA"?
|
||||
// If so, `resolveScopedName` failed.
|
||||
// Let's check output if test fails or just check existence.
|
||||
if strings.Contains(output, "Unused GAM: +FnA") {
|
||||
// This indicates scoping limitation or intended behavior if path is not full.
|
||||
// "Ref = FnA" vs "Ref = Functions.FnA".
|
||||
// MARTe scoping usually allows global search?
|
||||
// I added fallback to Root search in resolveScopedName.
|
||||
// FnA is child of Functions. Functions is child of App.
|
||||
// Root children: App.
|
||||
// App children: Functions.
|
||||
// Functions children: FnA.
|
||||
// Fallback checks `pt.Root.Children[name]`.
|
||||
// Name is "FnA".
|
||||
// Root children has "App". No "FnA".
|
||||
// So fallback fails.
|
||||
// So Index fails to resolve "FnA".
|
||||
// So "Unused GAM" warning IS expected given current Index logic.
|
||||
// I will NOT assert it is missing, unless I fix Index to search deep global (FindNode) as fallback?
|
||||
// Validator uses FindNode (Deep).
|
||||
// Index uses Scoped + Root Top Level.
|
||||
// If I want Index to match Validator, I should use FindNode as final fallback?
|
||||
// But that defeats scoping strictness.
|
||||
// Ideally `app_test.marte` should use `Functions.FnA` or `App.Functions.FnA`.
|
||||
// But for this test, I just check the requested diagnostics.
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ 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-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestLSPHoverDoc(t *testing.T) {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -3,8 +3,8 @@ 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-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestGetNodeContaining(t *testing.T) {
|
||||
|
||||
81
test/lsp_hover_datasource_test.go
Normal file
81
test/lsp_hover_datasource_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package integration
|
||||
|
||||
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 TestHoverDataSourceName(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
+DS1 = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM1 = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
S1 = {
|
||||
DataSource = DS1
|
||||
Alias = Sig1
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://test_ds.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse error: %v", err)
|
||||
}
|
||||
lsp.Tree.AddFile("test_ds.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Test 1: Explicit Signal (Sig1)
|
||||
// Position: "Sig1" at line 5 (0-based 4)
|
||||
// Line 4: " Sig1 = { Type = uint32 }"
|
||||
// Col: 8
|
||||
params1 := lsp.HoverParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 4, Character: 9},
|
||||
}
|
||||
|
||||
hover1 := lsp.HandleHover(params1)
|
||||
if hover1 == nil {
|
||||
t.Fatal("Expected hover for Sig1")
|
||||
}
|
||||
|
||||
content1 := hover1.Contents.(lsp.MarkupContent).Value
|
||||
// Expectation: explicit signal shows owner datasource
|
||||
if !strings.Contains(content1, "**DataSource**: `+DS1`") && !strings.Contains(content1, "**DataSource**: `DS1`") {
|
||||
t.Errorf("Expected DataSource: +DS1 in hover for Sig1, got: %s", content1)
|
||||
}
|
||||
|
||||
// Test 2: Implicit Signal (S1)
|
||||
// Position: "S1" at line 11 (0-based 10)
|
||||
params2 := lsp.HoverParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 10, Character: 9},
|
||||
}
|
||||
|
||||
hover2 := lsp.HandleHover(params2)
|
||||
if hover2 == nil {
|
||||
t.Fatal("Expected hover for S1")
|
||||
}
|
||||
|
||||
content2 := hover2.Contents.(lsp.MarkupContent).Value
|
||||
// Expectation: implicit signal shows referenced datasource
|
||||
if !strings.Contains(content2, "**DataSource**: `DS1`") {
|
||||
t.Errorf("Expected DataSource: DS1 in hover for S1, got: %s", content2)
|
||||
}
|
||||
}
|
||||
75
test/lsp_hover_gam_usage_test.go
Normal file
75
test/lsp_hover_gam_usage_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package integration
|
||||
|
||||
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 TestHoverGAMUsage(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
+DS1 = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM1 = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
S1 = {
|
||||
DataSource = DS1
|
||||
Alias = Sig1
|
||||
}
|
||||
}
|
||||
}
|
||||
+GAM2 = {
|
||||
Class = IOGAM
|
||||
+OutputSignals = {
|
||||
S2 = {
|
||||
DataSource = DS1
|
||||
Alias = Sig1
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://test_gam_usage.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("test_gam_usage.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Query hover for Sig1 (Line 5)
|
||||
// Line 4: Sig1... (0-based)
|
||||
params := lsp.HoverParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 4, Character: 9},
|
||||
}
|
||||
|
||||
hover := lsp.HandleHover(params)
|
||||
if hover == nil {
|
||||
t.Fatal("Expected hover")
|
||||
}
|
||||
|
||||
contentHover := hover.Contents.(lsp.MarkupContent).Value
|
||||
if !strings.Contains(contentHover, "**Used in GAMs**") {
|
||||
t.Errorf("Expected 'Used in GAMs' section, got:\n%s", contentHover)
|
||||
}
|
||||
if !strings.Contains(contentHover, "- +GAM1") {
|
||||
t.Error("Expected +GAM1 in usage list")
|
||||
}
|
||||
if !strings.Contains(contentHover, "- +GAM2") {
|
||||
t.Error("Expected +GAM2 in usage list")
|
||||
}
|
||||
}
|
||||
67
test/lsp_hover_variable_test.go
Normal file
67
test/lsp_hover_variable_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package integration
|
||||
|
||||
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 TestLSPHoverVariable(t *testing.T) {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
#var MyInt: int = 123
|
||||
+Obj = {
|
||||
Field = @MyInt
|
||||
}
|
||||
`
|
||||
uri := "file://hover_var.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("hover_var.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// 1. Hover on Definition (#var MyInt)
|
||||
// Line 2 (index 1). # is at 0. Name "MyInt" is at 5.
|
||||
paramsDef := lsp.HoverParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 1, Character: 5},
|
||||
}
|
||||
resDef := lsp.HandleHover(paramsDef)
|
||||
if resDef == nil {
|
||||
t.Fatal("Expected hover for definition")
|
||||
}
|
||||
contentDef := resDef.Contents.(lsp.MarkupContent).Value
|
||||
if !strings.Contains(contentDef, "Type: `int`") {
|
||||
t.Errorf("Hover def missing type. Got: %s", contentDef)
|
||||
}
|
||||
if !strings.Contains(contentDef, "Default: `123`") {
|
||||
t.Errorf("Hover def missing default value. Got: %s", contentDef)
|
||||
}
|
||||
|
||||
// 2. Hover on Reference (@MyInt)
|
||||
// Line 4 (index 3). @MyInt is at col 12.
|
||||
paramsRef := lsp.HoverParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 12},
|
||||
}
|
||||
resRef := lsp.HandleHover(paramsRef)
|
||||
if resRef == nil {
|
||||
t.Fatal("Expected hover for reference")
|
||||
}
|
||||
contentRef := resRef.Contents.(lsp.MarkupContent).Value
|
||||
if !strings.Contains(contentRef, "Type: `int`") {
|
||||
t.Errorf("Hover ref missing type. Got: %s", contentRef)
|
||||
}
|
||||
if !strings.Contains(contentRef, "Default: `123`") {
|
||||
t.Errorf("Hover ref missing default value. Got: %s", contentRef)
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
108
test/lsp_inlay_hint_test.go
Normal file
108
test/lsp_inlay_hint_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/validator"
|
||||
)
|
||||
|
||||
func TestLSPInlayHint(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
#let N : int= 10 + 5
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 NumberOfElements = 10 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
Expr = 10 + 20
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = DS }
|
||||
}
|
||||
}
|
||||
+Other = {
|
||||
Class = Controller
|
||||
Ref = DS
|
||||
VarRef = @N + 1
|
||||
}
|
||||
`
|
||||
uri := "file://inlay.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile("inlay.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
v := validator.NewValidator(lsp.Tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
params := lsp.InlayHintParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Range: lsp.Range{
|
||||
Start: lsp.Position{Line: 0, Character: 0},
|
||||
End: lsp.Position{Line: 20, Character: 0},
|
||||
},
|
||||
}
|
||||
|
||||
res := lsp.HandleInlayHint(params)
|
||||
if len(res) == 0 {
|
||||
t.Fatal("Expected inlay hints, got 0")
|
||||
}
|
||||
|
||||
foundTypeHint := false
|
||||
foundDSClassHint := false
|
||||
foundGeneralRefHint := false
|
||||
foundExprHint := false
|
||||
foundVarRefHint := false
|
||||
foundLetHint := false
|
||||
|
||||
for _, hint := range res {
|
||||
t.Logf("Hint: '%s' at Line %d, Col %d", hint.Label, hint.Position.Line, hint.Position.Character)
|
||||
if hint.Label == "::uint32[10x1]" {
|
||||
foundTypeHint = true
|
||||
}
|
||||
if hint.Label == "FileReader::" && hint.Position.Line == 12 { // Sig1 line (DS)
|
||||
foundDSClassHint = true
|
||||
}
|
||||
if hint.Label == "FileReader::" && hint.Position.Line == 17 { // Ref = DS line
|
||||
foundGeneralRefHint = true
|
||||
}
|
||||
if hint.Label == " => 30" {
|
||||
foundExprHint = true
|
||||
}
|
||||
if hint.Label == "(=> 15)" {
|
||||
foundVarRefHint = true
|
||||
}
|
||||
if hint.Label == " => 15" && hint.Position.Line == 1 { // #let N line
|
||||
foundLetHint = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundTypeHint {
|
||||
t.Error("Did not find signal type/size hint")
|
||||
}
|
||||
if !foundDSClassHint {
|
||||
t.Error("Did not find DataSource class hint")
|
||||
}
|
||||
if !foundGeneralRefHint {
|
||||
t.Error("Did not find general object reference hint")
|
||||
}
|
||||
if !foundExprHint {
|
||||
t.Error("Did not find expression evaluation hint")
|
||||
}
|
||||
if !foundVarRefHint {
|
||||
t.Error("Did not find variable reference evaluation hint")
|
||||
}
|
||||
if !foundLetHint {
|
||||
t.Error("Did not find #let expression evaluation hint")
|
||||
}
|
||||
}
|
||||
73
test/lsp_inout_test.go
Normal file
73
test/lsp_inout_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
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 TestLSPINOUTOrdering(t *testing.T) {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
// Mock schema if necessary, but we rely on internal schema
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".")
|
||||
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := `
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+A = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
A = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
B = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State = {
|
||||
Class =RealTimeState
|
||||
Threads = {
|
||||
+Th1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = {A}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://app.marte"
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
|
||||
})
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "INOUT Signal 'A'") {
|
||||
t.Error("LSP did not report INOUT ordering error")
|
||||
t.Log(output)
|
||||
}
|
||||
}
|
||||
66
test/lsp_inout_warning_test.go
Normal file
66
test/lsp_inout_warning_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
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 TestLSPINOUTWarning(t *testing.T) {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".")
|
||||
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := `
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+Producer = {
|
||||
Class = IOGAM
|
||||
OutputSignals = {
|
||||
ProducedSig = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State = {
|
||||
Class =RealTimeState
|
||||
Threads = {
|
||||
+Th1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = {Producer}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://warning.marte"
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
|
||||
})
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "produced in thread '+Th1' but never consumed") {
|
||||
t.Error("LSP did not report INOUT usage warning")
|
||||
t.Log(output)
|
||||
}
|
||||
}
|
||||
88
test/lsp_recursive_index_test.go
Normal file
88
test/lsp_recursive_index_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
)
|
||||
|
||||
func TestLSPRecursiveIndexing(t *testing.T) {
|
||||
// Setup directory structure
|
||||
rootDir, err := os.MkdirTemp("", "lsp_recursive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
// root/main.marte
|
||||
mainContent := `
|
||||
#package App
|
||||
+Main = {
|
||||
Ref = SubComp
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "main.marte"), []byte(mainContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// root/subdir/sub.marte
|
||||
subDir := filepath.Join(rootDir, "subdir")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
subContent := `
|
||||
#package App
|
||||
+SubComp = { Class = Component }
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(subDir, "sub.marte"), []byte(subContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initialize LSP
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
// Simulate ScanDirectory
|
||||
if err := lsp.Tree.ScanDirectory(rootDir); err != nil {
|
||||
t.Fatalf("ScanDirectory failed: %v", err)
|
||||
}
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Check if SubComp is in the tree
|
||||
// Root -> App -> SubComp
|
||||
appNode := lsp.Tree.Root.Children["App"]
|
||||
if appNode == nil {
|
||||
t.Fatal("App package not found")
|
||||
}
|
||||
|
||||
subComp := appNode.Children["SubComp"]
|
||||
if subComp == nil {
|
||||
t.Fatal("SubComp not found in tree (recursive scan failed)")
|
||||
}
|
||||
|
||||
mainURI := "file://" + filepath.Join(rootDir, "main.marte")
|
||||
|
||||
// Definition Request
|
||||
params := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: mainURI},
|
||||
Position: lsp.Position{Line: 3, Character: 12},
|
||||
}
|
||||
|
||||
res := lsp.HandleDefinition(params)
|
||||
if res == nil {
|
||||
t.Fatal("Definition not found for SubComp")
|
||||
}
|
||||
|
||||
locs, ok := res.([]lsp.Location)
|
||||
if !ok || len(locs) == 0 {
|
||||
t.Fatal("Expected location list")
|
||||
}
|
||||
|
||||
expectedFile := filepath.Join(subDir, "sub.marte")
|
||||
if locs[0].URI != "file://"+expectedFile {
|
||||
t.Errorf("Expected definition in %s, got %s", expectedFile, locs[0].URI)
|
||||
}
|
||||
}
|
||||
89
test/lsp_rename_implicit_test.go
Normal file
89
test/lsp_rename_implicit_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/validator"
|
||||
)
|
||||
|
||||
func TestRenameImplicitToDefinition(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
// Implicit usage
|
||||
Sig1 = { DataSource = DS }
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://rename_imp.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("rename_imp.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Run validator to link targets
|
||||
v := validator.NewValidator(lsp.Tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
// Rename Implicit Sig1 (Line 11, 0-based 11)
|
||||
// Line 11: " Sig1 = { DataSource = DS }"
|
||||
params := lsp.RenameParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 11, Character: 9},
|
||||
NewName: "NewSig",
|
||||
}
|
||||
|
||||
edit := lsp.HandleRename(params)
|
||||
if edit == nil {
|
||||
t.Fatal("Expected edits")
|
||||
}
|
||||
|
||||
edits := edit.Changes[uri]
|
||||
|
||||
// Expect:
|
||||
// 1. Rename Implicit Sig1 (Line 9) -> NewSig
|
||||
// 2. Rename Definition Sig1 (Line 4) -> NewSig
|
||||
|
||||
if len(edits) != 2 {
|
||||
t.Errorf("Expected 2 edits, got %d", len(edits))
|
||||
for _, e := range edits {
|
||||
t.Logf("Edit at line %d", e.Range.Start.Line)
|
||||
}
|
||||
}
|
||||
|
||||
foundDef := false
|
||||
foundImp := false
|
||||
for _, e := range edits {
|
||||
if e.Range.Start.Line == 4 {
|
||||
foundDef = true
|
||||
}
|
||||
if e.Range.Start.Line == 11 {
|
||||
foundImp = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundDef {
|
||||
t.Error("Definition not renamed")
|
||||
}
|
||||
if !foundImp {
|
||||
t.Error("Implicit usage not renamed")
|
||||
}
|
||||
}
|
||||
110
test/lsp_rename_signal_test.go
Normal file
110
test/lsp_rename_signal_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/marte-community/marte-dev-tools/internal/validator"
|
||||
)
|
||||
|
||||
func TestRenameSignalInGAM(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
// Implicit match
|
||||
Sig1 = { DataSource = DS }
|
||||
// Explicit Alias
|
||||
S2 = { DataSource = DS Alias = Sig1 }
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://rename_sig.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("rename_sig.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Run validator to populate Targets
|
||||
v := validator.NewValidator(lsp.Tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
// Rename DS.Sig1 to NewSig
|
||||
// Sig1 is at Line 5.
|
||||
// Line 0: empty
|
||||
// Line 1: +DS
|
||||
// Line 2: Class
|
||||
// Line 3: +Signals
|
||||
// Line 4: Sig1
|
||||
params := lsp.RenameParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 4, Character: 9}, // Sig1
|
||||
NewName: "NewSig",
|
||||
}
|
||||
|
||||
edit := lsp.HandleRename(params)
|
||||
if edit == nil {
|
||||
t.Fatal("Expected edits")
|
||||
}
|
||||
|
||||
edits := edit.Changes[uri]
|
||||
|
||||
// Expect:
|
||||
// 1. Definition of Sig1 in DS (Line 5) -> NewSig
|
||||
// 2. Definition of Sig1 in GAM (Line 10) -> NewSig (Implicit match)
|
||||
// 3. Alias reference in S2 (Line 12) -> NewSig
|
||||
|
||||
// Line 10: Sig1 = ... (0-based 9)
|
||||
// Line 12: S2 = ... Alias = Sig1 (0-based 11)
|
||||
|
||||
expectedCount := 3
|
||||
if len(edits) != expectedCount {
|
||||
t.Errorf("Expected %d edits, got %d", expectedCount, len(edits))
|
||||
for _, e := range edits {
|
||||
t.Logf("Edit: %s at %d", e.NewText, e.Range.Start.Line)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Implicit Signal Rename
|
||||
foundImplicit := false
|
||||
for _, e := range edits {
|
||||
if e.Range.Start.Line == 11 {
|
||||
if e.NewText == "NewSig" {
|
||||
foundImplicit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundImplicit {
|
||||
t.Error("Did not find implicit signal rename")
|
||||
}
|
||||
|
||||
// Check Alias Rename
|
||||
foundAlias := false
|
||||
for _, e := range edits {
|
||||
if e.Range.Start.Line == 13 {
|
||||
// Alias = Sig1. Range should cover Sig1.
|
||||
if e.NewText == "NewSig" {
|
||||
foundAlias = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundAlias {
|
||||
t.Error("Did not find alias reference rename")
|
||||
}
|
||||
}
|
||||
92
test/lsp_rename_test.go
Normal file
92
test/lsp_rename_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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 TestHandleRename(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
#package Some
|
||||
+MyNode = {
|
||||
Class = Type
|
||||
}
|
||||
+Consumer = {
|
||||
Link = MyNode
|
||||
PkgLink = Some.MyNode
|
||||
}
|
||||
`
|
||||
uri := "file://rename.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("rename.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Rename +MyNode to NewNode
|
||||
// +MyNode is at Line 2 (after #package)
|
||||
// Line 0: empty
|
||||
// Line 1: #package
|
||||
// Line 2: +MyNode
|
||||
params := lsp.RenameParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 2, Character: 4}, // +MyNode
|
||||
NewName: "NewNode",
|
||||
}
|
||||
|
||||
edit := lsp.HandleRename(params)
|
||||
if edit == nil {
|
||||
t.Fatal("Expected edits")
|
||||
}
|
||||
|
||||
edits := edit.Changes[uri]
|
||||
if len(edits) != 3 {
|
||||
t.Errorf("Expected 3 edits (Def, Link, PkgLink), got %d", len(edits))
|
||||
}
|
||||
|
||||
// Verify Definition change (+MyNode -> +NewNode)
|
||||
foundDef := false
|
||||
for _, e := range edits {
|
||||
if e.NewText == "+NewNode" {
|
||||
foundDef = true
|
||||
if e.Range.Start.Line != 2 {
|
||||
t.Errorf("Definition edit line wrong: %d", e.Range.Start.Line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundDef {
|
||||
t.Error("Did not find definition edit +NewNode")
|
||||
}
|
||||
|
||||
// Verify Link change (MyNode -> NewNode)
|
||||
foundLink := false
|
||||
for _, e := range edits {
|
||||
if e.NewText == "NewNode" && e.Range.Start.Line == 6 { // Link = MyNode
|
||||
foundLink = true
|
||||
}
|
||||
}
|
||||
if !foundLink {
|
||||
t.Error("Did not find Link edit")
|
||||
}
|
||||
|
||||
// Verify PkgLink change (Some.MyNode -> Some.NewNode)
|
||||
foundPkg := false
|
||||
for _, e := range edits {
|
||||
if e.NewText == "NewNode" && e.Range.Start.Line == 7 { // PkgLink = Some.MyNode
|
||||
foundPkg = true
|
||||
}
|
||||
}
|
||||
if !foundPkg {
|
||||
t.Error("Did not find PkgLink edit")
|
||||
}
|
||||
}
|
||||
199
test/lsp_server_test.go
Normal file
199
test/lsp_server_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 TestInitProjectScan(t *testing.T) {
|
||||
// 1. Setup temp dir with files
|
||||
tmpDir, err := os.MkdirTemp("", "lsp_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// File 1: Definition
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "def.marte"), []byte("#package Test.Common\n+Target = { Class = C }"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// File 2: Reference
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "ref.marte"), []byte("#package Test.Common\n+Source = { Class = C Link = Target }"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 2. Initialize
|
||||
lsp.Tree = index.NewProjectTree() // Reset global tree
|
||||
|
||||
initParams := lsp.InitializeParams{RootPath: tmpDir}
|
||||
paramsBytes, _ := json.Marshal(initParams)
|
||||
|
||||
msg := &lsp.JsonRpcMessage{
|
||||
Method: "initialize",
|
||||
Params: paramsBytes,
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
lsp.HandleMessage(msg)
|
||||
|
||||
// Query the reference in ref.marte at "Target"
|
||||
defParams := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + filepath.Join(tmpDir, "ref.marte")},
|
||||
Position: lsp.Position{Line: 1, Character: 29},
|
||||
}
|
||||
|
||||
res := lsp.HandleDefinition(defParams)
|
||||
if res == nil {
|
||||
t.Fatal("Definition not found via LSP after initialization")
|
||||
}
|
||||
|
||||
locs, ok := res.([]lsp.Location)
|
||||
if !ok {
|
||||
t.Fatalf("Expected []lsp.Location, got %T", res)
|
||||
}
|
||||
|
||||
if len(locs) == 0 {
|
||||
t.Fatal("No locations found")
|
||||
}
|
||||
|
||||
// Verify uri points to def.marte
|
||||
expectedURI := "file://" + filepath.Join(tmpDir, "def.marte")
|
||||
if locs[0].URI != expectedURI {
|
||||
t.Errorf("Expected URI %s, got %s", expectedURI, locs[0].URI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDefinition(t *testing.T) {
|
||||
// Reset tree for test
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
|
||||
content := `
|
||||
+MyObject = {
|
||||
Class = Type
|
||||
}
|
||||
+RefObject = {
|
||||
Class = Type
|
||||
RefField = MyObject
|
||||
}
|
||||
`
|
||||
path := "/test.marte"
|
||||
p := parser.NewParser(content)
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
lsp.Tree.AddFile(path, config)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
t.Logf("Refs: %d", len(lsp.Tree.References))
|
||||
for _, r := range lsp.Tree.References {
|
||||
t.Logf(" %s at %d:%d", r.Name, r.Position.Line, r.Position.Column)
|
||||
}
|
||||
|
||||
// Test Go to Definition on MyObject reference
|
||||
params := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: lsp.Position{Line: 6, Character: 15}, // "MyObject" in RefField = MyObject
|
||||
}
|
||||
|
||||
result := lsp.HandleDefinition(params)
|
||||
if result == nil {
|
||||
t.Fatal("HandleDefinition returned nil")
|
||||
}
|
||||
|
||||
locations, ok := result.([]lsp.Location)
|
||||
if !ok {
|
||||
t.Fatalf("Expected []lsp.Location, got %T", result)
|
||||
}
|
||||
|
||||
if len(locations) != 1 {
|
||||
t.Fatalf("Expected 1 location, got %d", len(locations))
|
||||
}
|
||||
|
||||
if locations[0].Range.Start.Line != 1 { // +MyObject is on line 2 (0-indexed 1)
|
||||
t.Errorf("Expected definition on line 1, got %d", locations[0].Range.Start.Line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleReferences(t *testing.T) {
|
||||
// Reset tree for test
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
|
||||
content := `
|
||||
+MyObject = {
|
||||
Class = Type
|
||||
}
|
||||
+RefObject = {
|
||||
Class = Type
|
||||
RefField = MyObject
|
||||
}
|
||||
+AnotherRef = {
|
||||
Ref = MyObject
|
||||
}
|
||||
`
|
||||
path := "/test.marte"
|
||||
p := parser.NewParser(content)
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
lsp.Tree.AddFile(path, config)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Test Find References for MyObject (triggered from its definition)
|
||||
params := lsp.ReferenceParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: lsp.Position{Line: 1, Character: 1}, // "+MyObject"
|
||||
Context: lsp.ReferenceContext{IncludeDeclaration: true},
|
||||
}
|
||||
|
||||
locations := lsp.HandleReferences(params)
|
||||
if len(locations) != 3 { // 1 declaration + 2 references
|
||||
t.Fatalf("Expected 3 locations, got %d", len(locations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLSPFormatting(t *testing.T) {
|
||||
// Setup
|
||||
content := `
|
||||
#package Proj.Main
|
||||
+Object={
|
||||
Field=1
|
||||
}
|
||||
`
|
||||
uri := "file:///test.marte"
|
||||
|
||||
// Open (populate Documents map)
|
||||
lsp.Documents[uri] = content
|
||||
|
||||
// Format
|
||||
params := lsp.DocumentFormattingParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
}
|
||||
|
||||
edits := lsp.HandleFormatting(params)
|
||||
|
||||
if len(edits) != 1 {
|
||||
t.Fatalf("Expected 1 edit, got %d", len(edits))
|
||||
}
|
||||
|
||||
newText := edits[0].NewText
|
||||
|
||||
expected := `#package Proj.Main
|
||||
|
||||
+Object = {
|
||||
Field = 1
|
||||
}
|
||||
`
|
||||
// Normalize newlines for comparison just in case
|
||||
if strings.TrimSpace(strings.ReplaceAll(newText, "\r\n", "\n")) != strings.TrimSpace(strings.ReplaceAll(expected, "\r\n", "\n")) {
|
||||
t.Errorf("Formatting mismatch.\nExpected:\n%s\nGot:\n%s", expected, newText)
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ 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"
|
||||
"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 TestLSPSignalReferences(t *testing.T) {
|
||||
@@ -47,18 +47,24 @@ func TestLSPSignalReferences(t *testing.T) {
|
||||
// Find definition of MySig in MyDS
|
||||
root := idx.IsolatedFiles["signal_refs.marte"]
|
||||
if root == nil {
|
||||
t.Fatal("Root node not found")
|
||||
t.Fatal("Root node not found (isolated)")
|
||||
}
|
||||
|
||||
// Traverse to MySig
|
||||
dataNode := root.Children["Data"]
|
||||
if dataNode == nil { t.Fatal("Data node not found") }
|
||||
if dataNode == nil {
|
||||
t.Fatal("Data node not found")
|
||||
}
|
||||
|
||||
myDS := dataNode.Children["MyDS"]
|
||||
if myDS == nil { t.Fatal("MyDS node not found") }
|
||||
if myDS == nil {
|
||||
t.Fatal("MyDS node not found")
|
||||
}
|
||||
|
||||
signals := myDS.Children["Signals"]
|
||||
if signals == nil { t.Fatal("Signals node not found") }
|
||||
if signals == nil {
|
||||
t.Fatal("Signals node not found")
|
||||
}
|
||||
|
||||
mySigDef := signals.Children["MySig"]
|
||||
if mySigDef == nil {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"io/ioutil"
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Helper to load and parse a file
|
||||
|
||||
77
test/lsp_validation_threading_test.go
Normal file
77
test/lsp_validation_threading_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
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
|
||||
#meta = {
|
||||
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)
|
||||
}
|
||||
}
|
||||
44
test/lsp_value_validation_test.go
Normal file
44
test/lsp_value_validation_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
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 TestLSPValueValidation(t *testing.T) {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".")
|
||||
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := `
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
+DS = { Class = GAMDataSource Signals = { S = { Type = uint8 } } }
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
S = { DataSource = DS Type = uint8 Value = 1024 }
|
||||
}
|
||||
}
|
||||
+App = { Class = RealTimeApplication +States = { Class = ReferenceContainer +S = { Class = RealTimeState Threads = { +T = { Class = RealTimeThread Functions = { GAM } } } } } }
|
||||
`
|
||||
uri := "file://value.marte"
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
|
||||
})
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "Value initialization mismatch") {
|
||||
t.Error("LSP did not report value validation error")
|
||||
t.Log(output)
|
||||
}
|
||||
}
|
||||
62
test/lsp_variable_refs_test.go
Normal file
62
test/lsp_variable_refs_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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 TestLSPVariableRefs(t *testing.T) {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
#var MyVar: int = 1
|
||||
+Obj = {
|
||||
Field = @MyVar
|
||||
}
|
||||
`
|
||||
uri := "file://vars.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lsp.Tree.AddFile("vars.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// 1. Definition from Usage
|
||||
// Line 4: " Field = @MyVar"
|
||||
// @ is at col 12 (0-based) ?
|
||||
// " Field = " is 4 + 6 + 3 = 13 chars?
|
||||
// 4 spaces. Field (5). " = " (3). 4+5+3 = 12.
|
||||
// So @ is at 12.
|
||||
paramsDef := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 12},
|
||||
}
|
||||
resDef := lsp.HandleDefinition(paramsDef)
|
||||
locs, ok := resDef.([]lsp.Location)
|
||||
if !ok || len(locs) != 1 {
|
||||
t.Fatalf("Expected 1 definition location, got %v", resDef)
|
||||
}
|
||||
// Line 2 in file is index 1.
|
||||
if locs[0].Range.Start.Line != 1 {
|
||||
t.Errorf("Expected definition at line 1, got %d", locs[0].Range.Start.Line)
|
||||
}
|
||||
|
||||
// 2. References from Definition
|
||||
// #var at line 2 (index 1). Col 0.
|
||||
paramsRef := lsp.ReferenceParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 1, Character: 1},
|
||||
Context: lsp.ReferenceContext{IncludeDeclaration: true},
|
||||
}
|
||||
resRef := lsp.HandleReferences(paramsRef)
|
||||
if len(resRef) != 2 { // Decl + Usage
|
||||
t.Errorf("Expected 2 references, got %d", len(resRef))
|
||||
}
|
||||
}
|
||||
92
test/operators_test.go
Normal file
92
test/operators_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestOperators(t *testing.T) {
|
||||
content := `
|
||||
#var A: int = 10
|
||||
#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
|
||||
p := parser.NewParser(content)
|
||||
_, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
// Check Builder Output
|
||||
f, _ := os.CreateTemp("", "ops.marte")
|
||||
f.WriteString(content)
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{f.Name()}, nil)
|
||||
|
||||
outF, _ := os.CreateTemp("", "out.marte")
|
||||
defer os.Remove(outF.Name())
|
||||
b.Build(outF)
|
||||
outF.Close()
|
||||
|
||||
outContent, _ := os.ReadFile(outF.Name())
|
||||
outStr := string(outContent)
|
||||
|
||||
if !strings.Contains(outStr, "Math = 30") {
|
||||
t.Errorf("Math failed. Got:\n%s", outStr)
|
||||
}
|
||||
// 10 + 20 * 2 = 50
|
||||
if !strings.Contains(outStr, "Precedence = 50") {
|
||||
t.Errorf("Precedence failed. Got:\n%s", outStr)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package parser_test
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestParserStrictness(t *testing.T) {
|
||||
@@ -1,7 +1,9 @@
|
||||
package parser
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestParseBasic(t *testing.T) {
|
||||
@@ -22,7 +24,7 @@ $Node2 = {
|
||||
Array = {1 2 3}
|
||||
}
|
||||
`
|
||||
p := NewParser(input)
|
||||
p := parser.NewParser(input)
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse error: %v", err)
|
||||
54
test/recursive_indexing_test.go
Normal file
54
test/recursive_indexing_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
)
|
||||
|
||||
func TestRecursiveIndexing(t *testing.T) {
|
||||
// Setup: root/level1/level2/deep.marte
|
||||
rootDir, _ := os.MkdirTemp("", "rec_index")
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
l1 := filepath.Join(rootDir, "level1")
|
||||
l2 := filepath.Join(l1, "level2")
|
||||
if err := os.MkdirAll(l2, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
content := "#package Deep\n+DeepObj = { Class = A }"
|
||||
if err := os.WriteFile(filepath.Join(l2, "deep.marte"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Also add a file in root to ensure mixed levels work
|
||||
os.WriteFile(filepath.Join(rootDir, "root.marte"), []byte("#package Root\n+RootObj = { Class = A }"), 0644)
|
||||
|
||||
// Scan
|
||||
tree := index.NewProjectTree()
|
||||
err := tree.ScanDirectory(rootDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify Deep
|
||||
deepPkg := tree.Root.Children["Deep"]
|
||||
if deepPkg == nil {
|
||||
t.Fatal("Package Deep not found")
|
||||
}
|
||||
if deepPkg.Children["DeepObj"] == nil {
|
||||
t.Fatal("DeepObj not found in Deep package")
|
||||
}
|
||||
|
||||
// Verify Root
|
||||
rootPkg := tree.Root.Children["Root"]
|
||||
if rootPkg == nil {
|
||||
t.Fatal("Package Root not found")
|
||||
}
|
||||
if rootPkg.Children["RootObj"] == nil {
|
||||
t.Fatal("RootObj not found in Root package")
|
||||
}
|
||||
}
|
||||
53
test/regex_variable_test.go
Normal file
53
test/regex_variable_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
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 TestRegexVariable(t *testing.T) {
|
||||
content := `
|
||||
#var IP: string & =~"^[0-9.]+$" = "127.0.0.1"
|
||||
#var BadIP: string & =~"^[0-9.]+$" = "abc"
|
||||
|
||||
+Obj = {
|
||||
IP = @IP
|
||||
}
|
||||
`
|
||||
// Test Validator
|
||||
pt := index.NewProjectTree()
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
pt.AddFile("regex.marte", cfg)
|
||||
|
||||
v := validator.NewValidator(pt, ".")
|
||||
v.CheckVariables()
|
||||
|
||||
foundError := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Variable 'BadIP' value mismatch") {
|
||||
foundError = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundError {
|
||||
t.Error("Expected error for BadIP")
|
||||
for _, d := range v.Diagnostics {
|
||||
t.Logf("Diag: %s", d.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// Test valid variable
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Variable 'IP' value mismatch") {
|
||||
t.Error("Unexpected error for IP")
|
||||
}
|
||||
}
|
||||
}
|
||||
65
test/scoping_test.go
Normal file
65
test/scoping_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestNameScoping(t *testing.T) {
|
||||
// App1 = { A = { Data = 10 } B = { Ref = A } }
|
||||
// App2 = { C = { Data = 10 } A = { Data = 12 } D = { Ref = A } }
|
||||
|
||||
content := `
|
||||
+App1 = {
|
||||
Class = App
|
||||
+A = { Class = Node Data = 10 }
|
||||
+B = { Class = Node Ref = A }
|
||||
}
|
||||
+App2 = {
|
||||
Class = App
|
||||
+C = { Class = Node Data = 10 }
|
||||
+A = { Class = Node Data = 12 }
|
||||
+D = { Class = Node Ref = A }
|
||||
}
|
||||
`
|
||||
pt := index.NewProjectTree()
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil { t.Fatal(err) }
|
||||
pt.AddFile("main.marte", cfg)
|
||||
|
||||
pt.ResolveReferences()
|
||||
|
||||
// Helper to find ref target
|
||||
findRefTarget := func(refName string, containerName string) *index.ProjectNode {
|
||||
for _, ref := range pt.References {
|
||||
if ref.Name == refName {
|
||||
container := pt.GetNodeContaining(ref.File, ref.Position)
|
||||
if container != nil && container.RealName == containerName {
|
||||
return ref.Target
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
targetB := findRefTarget("A", "+B")
|
||||
if targetB == nil {
|
||||
t.Fatal("Could not find reference A in +B")
|
||||
}
|
||||
// Check if targetB is App1.A
|
||||
if targetB.Parent == nil || targetB.Parent.RealName != "+App1" {
|
||||
t.Errorf("App1.B.Ref resolved to wrong target: %v (Parent %v)", targetB.RealName, targetB.Parent.RealName)
|
||||
}
|
||||
|
||||
targetD := findRefTarget("A", "+D")
|
||||
if targetD == nil {
|
||||
t.Fatal("Could not find reference A in +D")
|
||||
}
|
||||
// Check if targetD is App2.A
|
||||
if targetD.Parent == nil || targetD.Parent.RealName != "+App2" {
|
||||
t.Errorf("App2.D.Ref resolved to wrong target: %v (Parent %v)", targetD.RealName, targetD.Parent.RealName)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestMDSWriterValidation(t *testing.T) {
|
||||
@@ -38,7 +38,7 @@ func TestMDSWriterValidation(t *testing.T) {
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'TreeName'") {
|
||||
if strings.Contains(d.Message, "TreeName: incomplete value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestMathExpressionGAMValidation(t *testing.T) {
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Expression'") {
|
||||
if strings.Contains(d.Message, "Expression: incomplete value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestPIDGAMValidation(t *testing.T) {
|
||||
@@ -35,10 +35,10 @@ func TestPIDGAMValidation(t *testing.T) {
|
||||
foundKd := false
|
||||
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Ki'") {
|
||||
if strings.Contains(d.Message, "Ki: incomplete value") {
|
||||
foundKi = true
|
||||
}
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Kd'") {
|
||||
if strings.Contains(d.Message, "Kd: incomplete value") {
|
||||
foundKd = true
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func TestFileDataSourceValidation(t *testing.T) {
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Filename'") {
|
||||
if strings.Contains(d.Message, "Filename: incomplete value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
124
test/validator_datasource_threading_test.go
Normal file
124
test/validator_datasource_threading_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
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
|
||||
#meta = {
|
||||
direction = "INOUT"
|
||||
multithreaded = false
|
||||
}
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
+MultiDS = {
|
||||
Class = GAMDataSource
|
||||
#meta = {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestRealTimeApplicationValidation(t *testing.T) {
|
||||
@@ -35,14 +35,20 @@ func TestRealTimeApplicationValidation(t *testing.T) {
|
||||
missingStates := false
|
||||
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Data'") {
|
||||
if strings.Contains(d.Message, "Data: field is required") {
|
||||
missingData = true
|
||||
}
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'States'") {
|
||||
if strings.Contains(d.Message, "States: field is required") {
|
||||
missingStates = true
|
||||
}
|
||||
}
|
||||
|
||||
if !missingData || !missingStates {
|
||||
for _, d := range v.Diagnostics {
|
||||
t.Logf("Diagnostic: %s", d.Message)
|
||||
}
|
||||
}
|
||||
|
||||
if !missingData {
|
||||
t.Error("Expected error for missing 'Data' field in RealTimeApplication")
|
||||
}
|
||||
@@ -73,7 +79,7 @@ func TestGAMSchedulerValidation(t *testing.T) {
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'TimingDataSource'") {
|
||||
if strings.Contains(d.Message, "TimingDataSource: incomplete value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
84
test/validator_expression_test.go
Normal file
84
test/validator_expression_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestSDNSubscriberValidation(t *testing.T) {
|
||||
@@ -15,7 +15,7 @@ func TestSDNSubscriberValidation(t *testing.T) {
|
||||
+MySDN = {
|
||||
Class = SDNSubscriber
|
||||
Address = "239.0.0.1"
|
||||
// Missing Port
|
||||
// Missing Interface
|
||||
}
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
@@ -32,7 +32,7 @@ func TestSDNSubscriberValidation(t *testing.T) {
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Port'") {
|
||||
if strings.Contains(d.Message, "Interface: field is required but not present") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func TestFileWriterValidation(t *testing.T) {
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Missing mandatory field 'Filename'") {
|
||||
if strings.Contains(d.Message, "Filename: incomplete value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestFunctionsArrayValidation(t *testing.T) {
|
||||
|
||||
85
test/validator_gam_direction_test.go
Normal file
85
test/validator_gam_direction_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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 TestGAMSignalDirectionality(t *testing.T) {
|
||||
content := `
|
||||
$App = {
|
||||
$Data = {
|
||||
+InDS = { Class = FileReader Filename="f" +Signals = { S1 = { Type = uint32 } } }
|
||||
+OutDS = { Class = FileWriter Filename="f" +Signals = { S1 = { Type = uint32 } } }
|
||||
+InOutDS = { Class = FileDataSource Filename="f" +Signals = { S1 = { Type = uint32 } } }
|
||||
}
|
||||
+ValidGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
S1 = { DataSource = InDS }
|
||||
S2 = { DataSource = InOutDS Alias = S1 }
|
||||
}
|
||||
OutputSignals = {
|
||||
S3 = { DataSource = OutDS Alias = S1 }
|
||||
S4 = { DataSource = InOutDS Alias = S1 }
|
||||
}
|
||||
}
|
||||
+InvalidGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
BadIn = { DataSource = OutDS Alias = S1 }
|
||||
}
|
||||
OutputSignals = {
|
||||
BadOut = { DataSource = InDS Alias = S1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
idx := index.NewProjectTree()
|
||||
idx.AddFile("dir.marte", config)
|
||||
idx.ResolveReferences()
|
||||
|
||||
v := validator.NewValidator(idx, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
// Check ValidGAM has NO directionality errors
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "is Output-only but referenced in InputSignals") ||
|
||||
strings.Contains(d.Message, "is Input-only but referenced in OutputSignals") {
|
||||
if strings.Contains(d.Message, "ValidGAM") {
|
||||
t.Errorf("Unexpected direction error for ValidGAM: %s", d.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check InvalidGAM HAS errors
|
||||
foundBadIn := false
|
||||
foundBadOut := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "InvalidGAM") {
|
||||
if strings.Contains(d.Message, "is Output-only but referenced in InputSignals") {
|
||||
foundBadIn = true
|
||||
}
|
||||
if strings.Contains(d.Message, "is Input-only but referenced in OutputSignals") {
|
||||
foundBadOut = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundBadIn {
|
||||
t.Error("Expected error for OutDS in InputSignals of InvalidGAM")
|
||||
}
|
||||
if !foundBadOut {
|
||||
t.Error("Expected error for InDS in OutputSignals of InvalidGAM")
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ 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"
|
||||
"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 TestGAMSignalLinking(t *testing.T) {
|
||||
@@ -23,6 +23,7 @@ func TestGAMSignalLinking(t *testing.T) {
|
||||
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
//! ignore(unused)
|
||||
InputSignals = {
|
||||
MySig = {
|
||||
DataSource = MyDS
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestGAMSignalValidation(t *testing.T) {
|
||||
@@ -82,7 +82,7 @@ func TestGAMSignalValidation(t *testing.T) {
|
||||
if strings.Contains(d.Message, "DataSource 'OutDS' (Class FileWriter) is Output-only but referenced in InputSignals") {
|
||||
foundBadInput = true
|
||||
}
|
||||
if strings.Contains(d.Message, "Signal 'MissingSig' not found in DataSource 'InDS'") {
|
||||
if strings.Contains(d.Message, "Implicitly Defined Signal: 'MissingSig'") {
|
||||
foundMissing = true
|
||||
}
|
||||
if strings.Contains(d.Message, "DataSource 'InDS' (Class FileReader) is Input-only but referenced in OutputSignals") {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestGlobalPragmaDebug(t *testing.T) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestGlobalPragma(t *testing.T) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestGlobalPragmaUpdate(t *testing.T) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestIgnorePragma(t *testing.T) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"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"
|
||||
"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 TestImplicitSignal(t *testing.T) {
|
||||
|
||||
93
test/validator_inout_ordering_test.go
Normal file
93
test/validator_inout_ordering_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
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 TestINOUTOrdering(t *testing.T) {
|
||||
content := `
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
+MyDS = {
|
||||
Class = GAMDataSource
|
||||
#meta = { multithreaded = false } // Explicitly false
|
||||
Signals = { Sig1 = { Type = uint32 } }
|
||||
}
|
||||
}
|
||||
+GAM_Consumer = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = MyDS Type = uint32 }
|
||||
}
|
||||
}
|
||||
+GAM_Producer = {
|
||||
Class = IOGAM
|
||||
OutputSignals = {
|
||||
Sig1 = { DataSource = MyDS Type = uint32 }
|
||||
}
|
||||
}
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State1 = {
|
||||
Class = RealTimeState
|
||||
+Thread1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = { GAM_Consumer, GAM_Producer } // Fail
|
||||
}
|
||||
}
|
||||
+State2 = {
|
||||
Class = RealTimeState
|
||||
+Thread2 = {
|
||||
Class = RealTimeThread
|
||||
Functions = { GAM_Producer, GAM_Consumer } // Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
pt := index.NewProjectTree()
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pt.AddFile("main.marte", cfg)
|
||||
|
||||
// Use validator with default schema (embedded)
|
||||
// We pass "." but it shouldn't matter if no .marte_schema.cue exists
|
||||
v := validator.NewValidator(pt, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
foundError := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "consumed by GAM '+GAM_Consumer'") &&
|
||||
strings.Contains(d.Message, "before being produced") {
|
||||
foundError = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundError {
|
||||
t.Error("Expected INOUT ordering error for State1")
|
||||
for _, d := range v.Diagnostics {
|
||||
t.Logf("Diag: %s", d.Message)
|
||||
}
|
||||
}
|
||||
|
||||
foundErrorState2 := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "State '+State2'") && strings.Contains(d.Message, "before being produced") {
|
||||
foundErrorState2 = true
|
||||
}
|
||||
}
|
||||
|
||||
if foundErrorState2 {
|
||||
t.Error("Unexpected INOUT ordering error for State2 (Correct order)")
|
||||
}
|
||||
}
|
||||
101
test/validator_inout_value_test.go
Normal file
101
test/validator_inout_value_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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 TestINOUTValueInitialization(t *testing.T) {
|
||||
content := `
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
+MyDS = {
|
||||
Class = GAMDataSource
|
||||
#meta = { multithreaded = false }
|
||||
Signals = { Sig1 = { Type = uint32 } }
|
||||
}
|
||||
}
|
||||
+GAM1 = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = {
|
||||
DataSource = MyDS
|
||||
Type = uint32
|
||||
Value = 10 // Initialization
|
||||
}
|
||||
}
|
||||
}
|
||||
+GAM2 = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = MyDS Type = uint32 } // Consumes initialized signal
|
||||
}
|
||||
}
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State1 = {
|
||||
Class = RealTimeState
|
||||
+Thread1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = { GAM1, GAM2 } // Should Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
pt := index.NewProjectTree()
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pt.AddFile("main.marte", cfg)
|
||||
|
||||
v := validator.NewValidator(pt, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "before being produced") {
|
||||
t.Errorf("Unexpected error: %s", d.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestINOUTValueTypeMismatch(t *testing.T) {
|
||||
content := `
|
||||
+Data = { Class = ReferenceContainer +DS = { Class = GAMDataSource #meta = { multithreaded = false } Signals = { S = { Type = uint8 } } } }
|
||||
+GAM1 = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
S = { DataSource = DS Type = uint8 Value = 1024 }
|
||||
}
|
||||
}
|
||||
+App = { Class = RealTimeApplication +States = { Class = ReferenceContainer +S = { Class = RealTimeState Threads = { +T = { Class = RealTimeThread Functions = { GAM1 } } } } } }
|
||||
`
|
||||
pt := index.NewProjectTree()
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pt.AddFile("fail.marte", cfg)
|
||||
|
||||
v := validator.NewValidator(pt, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "Value initialization mismatch") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected Value initialization mismatch error")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user