2024-04-27 00:11:22 +00:00
|
|
|
package ucl_test
|
2024-04-11 12:05:05 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2025-05-24 23:50:25 +00:00
|
|
|
"strings"
|
2025-04-10 11:35:12 +00:00
|
|
|
"ucl.lmika.dev/ucl"
|
2024-04-27 00:11:22 +00:00
|
|
|
|
2024-04-11 12:05:05 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"testing"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestInst_Eval(t *testing.T) {
|
|
|
|
tests := []struct {
|
2025-01-15 11:07:29 +00:00
|
|
|
desc string
|
|
|
|
expr string
|
|
|
|
want any
|
2025-05-23 23:52:10 +00:00
|
|
|
wantObj bool
|
2025-01-15 11:07:29 +00:00
|
|
|
wantErr error
|
2024-04-11 12:05:05 +00:00
|
|
|
}{
|
|
|
|
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
|
2024-04-24 10:12:39 +00:00
|
|
|
{desc: "simple int 1", expr: `firstarg 123`, want: 123},
|
|
|
|
{desc: "simple int 2", expr: `firstarg -234`, want: -234},
|
|
|
|
{desc: "simple ident", expr: `firstarg a-test`, want: "a-test"},
|
2024-04-11 12:05:05 +00:00
|
|
|
|
2025-01-15 11:07:29 +00:00
|
|
|
// String interpolation
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "interpolate string 1", expr: `$what = "world" ; firstarg "hello $what"`, want: "hello world"},
|
|
|
|
{desc: "interpolate string 2", expr: `$what = "world" ; $when = "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
|
|
|
|
{desc: "interpolate string 3", expr: `$what = "world" ; $when = "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
|
|
|
|
{desc: "interpolate string 4", expr: `$crazy = [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
|
|
|
|
{desc: "interpolate string 5", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
|
|
|
|
{desc: "interpolate string 6", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
|
|
|
|
{desc: "interpolate string 7", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"},
|
|
|
|
{desc: "interpolate string 8", expr: `$words = ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"},
|
|
|
|
{desc: "interpolate string 9", expr: `$what = "world" ; firstarg "hello $($what)"`, want: "hello world"},
|
2025-01-17 22:54:21 +00:00
|
|
|
{desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
|
|
|
|
{desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
|
|
|
|
{desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
|
2025-01-15 11:07:29 +00:00
|
|
|
|
2024-04-11 12:05:05 +00:00
|
|
|
// Sub-expressions
|
2024-04-12 23:25:16 +00:00
|
|
|
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},
|
|
|
|
{desc: "sub expression 2", expr: `firstarg (sjoin "hello " "world")`, want: "hello world"},
|
|
|
|
{desc: "sub expression 3", expr: `firstarg (sjoin "hello" (sjoin " ") (sjoin "world"))`, want: "hello world"},
|
2024-04-11 12:05:05 +00:00
|
|
|
|
|
|
|
// Variables
|
|
|
|
{desc: "var 1", expr: `firstarg $a`, want: "alpha"},
|
|
|
|
{desc: "var 2", expr: `firstarg $bee`, want: "buzz"},
|
2024-04-12 23:25:16 +00:00
|
|
|
{desc: "var 3", expr: `firstarg (sjoin $bee " " $bee " " $bee)`, want: "buzz buzz buzz"},
|
2024-04-11 12:05:05 +00:00
|
|
|
|
|
|
|
// Pipeline
|
2024-04-23 12:02:06 +00:00
|
|
|
{desc: "pipe 1", expr: `list "aye" "bee" "see" | joinpipe`, want: "aye,bee,see"},
|
|
|
|
{desc: "pipe 2", expr: `list "aye" "bee" "see" | map { |x| toUpper $x } | joinpipe`, want: "AYE,BEE,SEE"},
|
|
|
|
{desc: "pipe 3", expr: `firstarg ["normal"] | map { |x| toUpper $x } | joinpipe`, want: "NORMAL"},
|
|
|
|
{desc: "pipe literal 1", expr: `"hello" | firstarg`, want: "hello"},
|
|
|
|
{desc: "pipe literal 2", expr: `["hello" "world"] | joinpipe`, want: "hello,world"},
|
2024-04-11 12:05:05 +00:00
|
|
|
|
2024-04-23 12:02:06 +00:00
|
|
|
{desc: "ignored pipe", expr: `(list "aye" | firstarg "ignore me") | joinpipe`, want: "aye"},
|
2024-04-11 12:05:05 +00:00
|
|
|
|
|
|
|
// Multi-statements
|
|
|
|
{desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"},
|
2024-04-23 12:02:06 +00:00
|
|
|
{desc: "multi 2", expr: `list "hello" | toUpper ; firstarg "world"`, want: "world"},
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "multi 3", expr: `$new = "this is new" ; firstarg $new`, want: "this is new"},
|
2024-04-16 12:05:21 +00:00
|
|
|
|
|
|
|
// Lists
|
|
|
|
{desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}},
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "list 2", expr: `$one = "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}},
|
2024-04-16 12:05:21 +00:00
|
|
|
{desc: "list 3", expr: `firstarg []`, want: []any{}},
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "list 4", expr: `$x = ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}},
|
2024-04-16 12:05:21 +00:00
|
|
|
|
|
|
|
// Maps
|
|
|
|
{desc: "map 1", expr: `firstarg [one:"1" two:"2" three:"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}},
|
|
|
|
{desc: "map 2", expr: `firstarg ["one":"1" "two":"2" "three":"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}},
|
|
|
|
{desc: "map 3", expr: `
|
2025-05-18 00:42:32 +00:00
|
|
|
$one = "one" ; $n1 = "1"
|
2024-04-16 12:05:21 +00:00
|
|
|
firstarg [
|
|
|
|
$one:$n1
|
2024-04-23 12:02:06 +00:00
|
|
|
(list "two" | map { |x| toUpper $x } | head):(list "2" | map { |x| toUpper $x } | head)
|
2024-04-16 12:05:21 +00:00
|
|
|
three:"3"
|
|
|
|
]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}},
|
|
|
|
{desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}},
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "map 5", expr: `$x = ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}},
|
|
|
|
{desc: "map 6", expr: `$x = [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}},
|
2024-05-10 23:16:34 +00:00
|
|
|
|
|
|
|
// Dots
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "dot expr 1", expr: `$x = [1 2 3] ; $x.(0)`, want: 1},
|
|
|
|
{desc: "dot expr 2", expr: `$x = [1 2 3] ; $x.(1)`, want: 2},
|
|
|
|
{desc: "dot expr 3", expr: `$x = [1 2 3] ; $x.(2)`, want: 3},
|
|
|
|
{desc: "dot expr 4", expr: `$x = [1 2 3] ; $x.(3)`, want: nil},
|
|
|
|
{desc: "dot expr 5", expr: `$x = [1 2 3] ; $x.(add 1 1)`, want: 3},
|
|
|
|
{desc: "dot expr 6", expr: `$x = [1 2 3] ; $x.(-1)`, want: 3},
|
|
|
|
{desc: "dot expr 7", expr: `$x = [1 2 3] ; $x.(-2)`, want: 2},
|
|
|
|
{desc: "dot expr 8", expr: `$x = [1 2 3] ; $x.(-3)`, want: 1},
|
|
|
|
{desc: "dot expr 9", expr: `$x = [1 2 3] ; $x.(-4)`, want: nil},
|
|
|
|
|
|
|
|
{desc: "dot idents 1", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"},
|
|
|
|
{desc: "dot idents 2", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"},
|
|
|
|
{desc: "dot idents 3", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil},
|
|
|
|
{desc: "dot idents 4", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"},
|
|
|
|
{desc: "dot idents 5", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"},
|
|
|
|
{desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil},
|
|
|
|
{desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"},
|
|
|
|
{desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"},
|
|
|
|
{desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; x.y`, want: nil},
|
2025-01-15 11:07:29 +00:00
|
|
|
|
2025-05-23 23:52:10 +00:00
|
|
|
{desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil},
|
|
|
|
{desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil},
|
|
|
|
{desc: "parse comments 3", expr: parseComments3, wantObj: true, wantErr: nil},
|
|
|
|
{desc: "parse comments 4", expr: parseComments4, wantObj: true, wantErr: nil},
|
2024-04-11 12:05:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
outW := bytes.NewBuffer(nil)
|
|
|
|
|
2024-04-27 00:11:22 +00:00
|
|
|
inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin())
|
2025-05-24 23:50:25 +00:00
|
|
|
res, err := inst.EvalString(ctx, tt.expr)
|
2024-04-11 12:05:05 +00:00
|
|
|
|
2025-01-15 11:07:29 +00:00
|
|
|
if tt.wantErr != nil {
|
|
|
|
assert.ErrorIs(t, err, tt.wantErr)
|
2025-05-23 23:52:10 +00:00
|
|
|
} else if tt.wantObj {
|
|
|
|
assert.NoError(t, err)
|
|
|
|
_, isObj := res.(ucl.Object)
|
|
|
|
assert.True(t, isObj)
|
2025-01-15 11:07:29 +00:00
|
|
|
} else {
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.want, res)
|
|
|
|
}
|
2024-04-11 12:05:05 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2025-01-15 11:07:29 +00:00
|
|
|
|
2025-05-24 23:50:25 +00:00
|
|
|
func TestInst_Eval_WithSubEnv(t *testing.T) {
|
|
|
|
t.Run("global symbols should not leak across environments", func(t *testing.T) {
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
inst := ucl.New()
|
|
|
|
|
|
|
|
res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; $a`), ucl.WithSubEnv())
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "hello", res)
|
|
|
|
|
|
|
|
res, err = inst.Eval(ctx, strings.NewReader(`$a`), ucl.WithSubEnv())
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, res)
|
|
|
|
})
|
|
|
|
|
2025-05-25 00:08:56 +00:00
|
|
|
t.Run("environments should not leak when using hooks", func(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
descr string
|
|
|
|
eval1 string
|
|
|
|
eval2 string
|
|
|
|
want1 any
|
|
|
|
want2 any
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
descr: "reading vars",
|
|
|
|
eval1: `$a = "hello" ; hook { $a }`,
|
|
|
|
eval2: `$a = "world" ; hook { $a }`,
|
|
|
|
want1: "hello",
|
|
|
|
want2: "world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
descr: "modifying vars",
|
|
|
|
eval1: `$a = "hello" ; hook { $a = "new value" ; $a }`,
|
|
|
|
eval2: `$a = "world" ; hook { $a }`,
|
|
|
|
want1: "new value",
|
|
|
|
want2: "world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
descr: "defining procs",
|
|
|
|
eval1: `proc say_hello { "hello" } ; hook { say_hello }`,
|
|
|
|
eval2: `proc say_hello { "world" } ; hook { say_hello }`,
|
|
|
|
want1: "hello",
|
|
|
|
want2: "world",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.descr, func(t *testing.T) {
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
hooks := make([]ucl.Invokable, 0)
|
|
|
|
|
|
|
|
inst := ucl.New()
|
|
|
|
inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) {
|
|
|
|
var hookProc ucl.Invokable
|
|
|
|
|
|
|
|
if err := args.Bind(&hookProc); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
hooks = append(hooks, hookProc)
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
})
|
|
|
|
|
|
|
|
res, err := inst.Eval(ctx, strings.NewReader(tt.eval1), ucl.WithSubEnv())
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, res)
|
2025-05-24 23:50:25 +00:00
|
|
|
|
2025-05-25 00:08:56 +00:00
|
|
|
res, err = inst.Eval(ctx, strings.NewReader(tt.eval2), ucl.WithSubEnv())
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, res)
|
2025-05-24 23:50:25 +00:00
|
|
|
|
2025-05-25 00:08:56 +00:00
|
|
|
h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.want1, h1)
|
2025-05-24 23:50:25 +00:00
|
|
|
|
2025-05-25 00:08:56 +00:00
|
|
|
h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.want2, h2)
|
|
|
|
})
|
|
|
|
}
|
2025-05-24 23:50:25 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2025-05-17 11:51:16 +00:00
|
|
|
func TestInst_SetPseudoVar(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
desc string
|
|
|
|
expr string
|
|
|
|
wantRes any
|
|
|
|
wantBarVar string
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
{desc: "read var 1", expr: `@foo`, wantRes: "this is foo"},
|
|
|
|
{desc: "read var 2", expr: `@bar`, wantRes: "this is bar"},
|
|
|
|
{desc: "read var 3", expr: `@fla`, wantRes: "missing value of fla"},
|
|
|
|
|
2025-05-18 00:42:32 +00:00
|
|
|
{desc: "write var 1", expr: `@foo = "hello" ; @foo`, wantRes: "hello"},
|
|
|
|
{desc: "write var 2", expr: `@bar = "world" ; @bar`, wantRes: "world", wantBarVar: "world"},
|
|
|
|
{desc: "write var 3", expr: `@blong = "hello"`, wantErr: true},
|
2025-05-17 11:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
|
|
bar := &stringPseudoVar{str: "this is bar"}
|
|
|
|
|
|
|
|
inst := ucl.New(ucl.WithTestBuiltin())
|
|
|
|
inst.SetPseudoVar("foo", &stringPseudoVar{str: "this is foo"})
|
|
|
|
inst.SetPseudoVar("bar", bar)
|
|
|
|
inst.SetMissingPseudoVarHandler(missingPseudoVarType{})
|
|
|
|
|
2025-05-24 23:50:25 +00:00
|
|
|
res, err := inst.EvalString(t.Context(), tt.expr)
|
2025-05-17 11:51:16 +00:00
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
assert.Error(t, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.wantRes, res)
|
|
|
|
|
|
|
|
if tt.wantBarVar != "" {
|
|
|
|
assert.Equal(t, tt.wantBarVar, bar.str)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-15 11:07:29 +00:00
|
|
|
var parseComments1 = `
|
|
|
|
proc lookup { |file|
|
|
|
|
foreach { |toks|
|
|
|
|
}
|
|
|
|
# this use to fail
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
var parseComments2 = `
|
|
|
|
proc lookup { |file|
|
|
|
|
foreach { |toks|
|
|
|
|
}
|
|
|
|
|
|
|
|
# this use to fail
|
|
|
|
#
|
|
|
|
# And so did this
|
|
|
|
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
var parseComments3 = `
|
|
|
|
proc lookup { |file|
|
|
|
|
foreach { |toks|
|
|
|
|
}
|
|
|
|
|
|
|
|
# this use to fail
|
|
|
|
#
|
|
|
|
# And so did this
|
|
|
|
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
var parseComments4 = `
|
|
|
|
proc lookup { |file|
|
|
|
|
foreach { |toks|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
# this use to fail`
|
2025-05-17 11:51:16 +00:00
|
|
|
|
|
|
|
type stringPseudoVar struct {
|
|
|
|
str string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *stringPseudoVar) Get(ctx context.Context) (any, error) {
|
|
|
|
return s.str, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *stringPseudoVar) Set(ctx context.Context, v any) error {
|
|
|
|
s.str = v.(string)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type missingPseudoVarType struct{}
|
|
|
|
|
|
|
|
func (missingPseudoVarType) Get(ctx context.Context, name string) (any, error) {
|
|
|
|
return "missing value of " + name, nil
|
|
|
|
}
|