feat: pattern support args and multiple variants; add zed extension for highlight
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 13s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 8s
build-dsl-smoke / release / clang / linux (push) Successful in 9s
build-dsl-smoke / release / gcc / linux (push) Successful in 7s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 8s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 7s
build-dsl-smoke / debug / clang / windows (push) Successful in 16s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / clang / windows (push) Successful in 17s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 5s
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 13s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 8s
build-dsl-smoke / release / clang / linux (push) Successful in 9s
build-dsl-smoke / release / gcc / linux (push) Successful in 7s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 8s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 7s
build-dsl-smoke / debug / clang / windows (push) Successful in 16s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / clang / windows (push) Successful in 17s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 5s
This commit is contained in:
@@ -50,6 +50,8 @@ type Pattern struct {
|
||||
DirsGlob string
|
||||
InputFile string
|
||||
OutputFile string
|
||||
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (p *Pattern) IsDirMode() bool {
|
||||
|
||||
210
dsl/include_test.go
Normal file
210
dsl/include_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTempJdg(t *testing.T, dir, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestIncludeBasicMerge(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "common.jdg", `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
|
||||
build_defaults {
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
sources = "*.c"
|
||||
output = "solution"
|
||||
warnings = strict
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "common.jdg"
|
||||
|
||||
build "release" { profile = release }
|
||||
|
||||
group("g") {
|
||||
weight = 1.0
|
||||
test("t") { stdout = "ok\n" }
|
||||
}
|
||||
`)
|
||||
|
||||
f, _, err := ParseFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
if len(f.Toolchains) != 1 || f.Toolchains[0].Name != "gcc" {
|
||||
t.Errorf("toolchains not merged: %+v", f.Toolchains)
|
||||
}
|
||||
if f.BuildDefaults == nil || f.BuildDefaults.Language != "c" {
|
||||
t.Errorf("build_defaults not merged: %+v", f.BuildDefaults)
|
||||
}
|
||||
if len(f.Builds) != 1 || f.Builds[0].Name != "release" {
|
||||
t.Errorf("local build lost: %+v", f.Builds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeLocalOverridesIncluded(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "common.jdg", `
|
||||
build_defaults {
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
warnings = strict
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "common.jdg"
|
||||
|
||||
build_defaults {
|
||||
standard = "c17"
|
||||
}
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
f, _, err := ParseFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if f.BuildDefaults.Standard != "c17" {
|
||||
t.Errorf("local should override included standard: got %q", f.BuildDefaults.Standard)
|
||||
}
|
||||
if f.BuildDefaults.Language != "c" {
|
||||
t.Errorf("language should survive from include: got %q", f.BuildDefaults.Language)
|
||||
}
|
||||
if f.BuildDefaults.Warnings != WarningsStrict {
|
||||
t.Errorf("warnings should survive from include: got %v", f.BuildDefaults.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeRelativePathResolution(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "shared/tools.jdg", `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "suite/main.jdg", `
|
||||
include "../shared/tools.jdg"
|
||||
|
||||
build "release" {
|
||||
language = "c"
|
||||
sources = "solution.c"
|
||||
output = "solution"
|
||||
profile = release
|
||||
}
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
f, _, err := ParseFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if len(f.Toolchains) != 1 {
|
||||
t.Errorf("relative include failed to resolve: %+v", f.Toolchains)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeDuplicateToolchainErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "common.jdg", `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "common.jdg"
|
||||
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
_, _, err := ParseFile(mainPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate toolchain error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "duplicate toolchain") {
|
||||
t.Errorf("error %q does not mention duplicate toolchain", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeCircularDetection(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
aPath := writeTempJdg(t, dir, "a.jdg", `
|
||||
include "b.jdg"
|
||||
group("ga") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
writeTempJdg(t, dir, "b.jdg", `
|
||||
include "a.jdg"
|
||||
`)
|
||||
|
||||
_, _, err := ParseFile(aPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected circular include error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "circular include") {
|
||||
t.Errorf("error %q does not mention circular include", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeMissingFileErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "nonexistent.jdg"
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
_, _, err := ParseFile(mainPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing include error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "nonexistent.jdg") {
|
||||
t.Errorf("error %q does not reference the missing path", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsIncludeWithoutFileContext(t *testing.T) {
|
||||
src := `
|
||||
include "common.jdg"
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`
|
||||
_, _, err := Parse(src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error: include without file context")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "file context") {
|
||||
t.Errorf("error %q does not mention file context", err.Error())
|
||||
}
|
||||
}
|
||||
81
dsl/merge.go
Normal file
81
dsl/merge.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package dsl
|
||||
|
||||
import "fmt"
|
||||
|
||||
func mergeFiles(dst, src *File) error {
|
||||
if src.Build != "" {
|
||||
dst.Build = src.Build
|
||||
}
|
||||
if src.BuildLinux != "" {
|
||||
dst.BuildLinux = src.BuildLinux
|
||||
}
|
||||
if src.BuildWindows != "" {
|
||||
dst.BuildWindows = src.BuildWindows
|
||||
}
|
||||
if src.BuildDarwin != "" {
|
||||
dst.BuildDarwin = src.BuildDarwin
|
||||
}
|
||||
|
||||
if src.BuildDefaults != nil {
|
||||
if dst.BuildDefaults == nil {
|
||||
dst.BuildDefaults = &BuildConfig{}
|
||||
}
|
||||
dst.BuildDefaults.MergeFrom(src.BuildDefaults)
|
||||
}
|
||||
|
||||
seenTC := map[string]bool{}
|
||||
for _, t := range dst.Toolchains {
|
||||
seenTC[t.Name] = true
|
||||
}
|
||||
for _, t := range src.Toolchains {
|
||||
if seenTC[t.Name] {
|
||||
return fmt.Errorf("duplicate toolchain %q", t.Name)
|
||||
}
|
||||
seenTC[t.Name] = true
|
||||
dst.Toolchains = append(dst.Toolchains, t)
|
||||
}
|
||||
|
||||
seenB := map[string]bool{}
|
||||
for _, b := range dst.Builds {
|
||||
seenB[b.Name] = true
|
||||
}
|
||||
for _, b := range src.Builds {
|
||||
if seenB[b.Name] {
|
||||
return fmt.Errorf("duplicate build %q", b.Name)
|
||||
}
|
||||
seenB[b.Name] = true
|
||||
dst.Builds = append(dst.Builds, b)
|
||||
}
|
||||
|
||||
seenG := map[string]bool{}
|
||||
for _, g := range dst.Groups {
|
||||
seenG[g.Name] = true
|
||||
}
|
||||
for _, g := range src.Groups {
|
||||
if seenG[g.Name] {
|
||||
return fmt.Errorf("duplicate group %q", g.Name)
|
||||
}
|
||||
seenG[g.Name] = true
|
||||
dst.Groups = append(dst.Groups, g)
|
||||
}
|
||||
|
||||
if src.Timeout != 0 {
|
||||
dst.Timeout = src.Timeout
|
||||
}
|
||||
if src.MemoryLimit != 0 {
|
||||
dst.MemoryLimit = src.MemoryLimit
|
||||
}
|
||||
if src.Binary != "" {
|
||||
dst.Binary = src.Binary
|
||||
}
|
||||
if src.Sources != "" {
|
||||
dst.Sources = src.Sources
|
||||
}
|
||||
if src.NormalizeCRLF {
|
||||
dst.NormalizeCRLF = true
|
||||
}
|
||||
if src.TrimTrailingWS {
|
||||
dst.TrimTrailingWS = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
143
dsl/parser.go
143
dsl/parser.go
@@ -3,14 +3,18 @@ package dsl
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
tokens []Token
|
||||
pos int
|
||||
warns []string
|
||||
tokens []Token
|
||||
pos int
|
||||
warns []string
|
||||
includeBaseDir string
|
||||
visited map[string]bool
|
||||
}
|
||||
|
||||
func NewParser(tokens []Token) *Parser {
|
||||
@@ -72,9 +76,67 @@ func Parse(src string) (*File, []string, error) {
|
||||
if err != nil {
|
||||
return nil, parser.Warnings(), err
|
||||
}
|
||||
if err := parser.finalize(file); err != nil {
|
||||
return nil, parser.Warnings(), err
|
||||
}
|
||||
return file, parser.Warnings(), nil
|
||||
}
|
||||
|
||||
func ParseFile(path string) (*File, []string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resolve %q: %w", path, err)
|
||||
}
|
||||
visited := map[string]bool{}
|
||||
parser, file, err := parseFileOnDisk(abs, visited)
|
||||
if err != nil {
|
||||
var warns []string
|
||||
if parser != nil {
|
||||
warns = parser.Warnings()
|
||||
}
|
||||
return nil, warns, err
|
||||
}
|
||||
if err := parser.finalize(file); err != nil {
|
||||
return nil, parser.Warnings(), err
|
||||
}
|
||||
return file, parser.Warnings(), nil
|
||||
}
|
||||
|
||||
func parseFileOnDisk(absPath string, visited map[string]bool) (*Parser, *File, error) {
|
||||
if visited[absPath] {
|
||||
return nil, nil, fmt.Errorf("circular include: %s", absPath)
|
||||
}
|
||||
visited[absPath] = true
|
||||
|
||||
src, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read %s: %w", absPath, err)
|
||||
}
|
||||
tokens, err := NewLexer(string(src)).Tokenize()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", absPath, err)
|
||||
}
|
||||
p := NewParser(tokens)
|
||||
p.includeBaseDir = filepath.Dir(absPath)
|
||||
p.visited = visited
|
||||
|
||||
file, err := p.parseFile()
|
||||
if err != nil {
|
||||
return p, nil, fmt.Errorf("%s: %w", absPath, err)
|
||||
}
|
||||
return p, file, nil
|
||||
}
|
||||
|
||||
func (p *Parser) finalize(f *File) error {
|
||||
if err := p.validateWeights(f); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateBuilds(f); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFile() (*File, error) {
|
||||
f := &File{}
|
||||
|
||||
@@ -107,7 +169,10 @@ func (p *Parser) parseFile() (*File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.BuildDefaults = bc
|
||||
if f.BuildDefaults == nil {
|
||||
f.BuildDefaults = &BuildConfig{}
|
||||
}
|
||||
f.BuildDefaults.MergeFrom(bc)
|
||||
|
||||
case "toolchains":
|
||||
p.advance()
|
||||
@@ -115,7 +180,45 @@ func (p *Parser) parseFile() (*File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Toolchains = specs
|
||||
existing := map[string]bool{}
|
||||
for _, tc := range f.Toolchains {
|
||||
existing[tc.Name] = true
|
||||
}
|
||||
for _, tc := range specs {
|
||||
if existing[tc.Name] {
|
||||
return nil, fmt.Errorf("duplicate toolchain %q", tc.Name)
|
||||
}
|
||||
existing[tc.Name] = true
|
||||
f.Toolchains = append(f.Toolchains, tc)
|
||||
}
|
||||
|
||||
case "include":
|
||||
p.advance()
|
||||
s, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.includeBaseDir == "" {
|
||||
return nil, fmt.Errorf("%d:%d: `include` requires file context (use ParseFile, not Parse)", s.Line, s.Col)
|
||||
}
|
||||
target := s.Value
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(p.includeBaseDir, target)
|
||||
}
|
||||
abs, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: resolve include %q: %w", s.Line, s.Col, s.Value, err)
|
||||
}
|
||||
childParser, child, err := parseFileOnDisk(abs, p.visited)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: include %q: %w", s.Line, s.Col, s.Value, err)
|
||||
}
|
||||
for _, w := range childParser.Warnings() {
|
||||
p.warn(w)
|
||||
}
|
||||
if err := mergeFiles(f, child); err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: include %q: %w", s.Line, s.Col, s.Value, err)
|
||||
}
|
||||
|
||||
case "build_linux":
|
||||
p.advance()
|
||||
@@ -216,13 +319,6 @@ func (p *Parser) parseFile() (*File, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.validateWeights(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateBuilds(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
@@ -683,25 +779,40 @@ func (p *Parser) parsePattern() (*Pattern, error) {
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch t.Value {
|
||||
case "input":
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pat.DirsGlob != "" {
|
||||
pat.InputFile = val.Value
|
||||
} else {
|
||||
pat.InputGlob = val.Value
|
||||
}
|
||||
case "output":
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pat.DirsGlob != "" {
|
||||
pat.OutputFile = val.Value
|
||||
} else {
|
||||
pat.OutputGlob = val.Value
|
||||
}
|
||||
case "dirs":
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pat.DirsGlob = val.Value
|
||||
case "args":
|
||||
xs, err := p.parseStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pat.Args = xs
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unknown pattern field %q", t.Line, t.Col, t.Value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user