This enables imports in mcl code, and is one of last remaining blockers to using mgmt. Now we can start writing standalone modules, and adding standard library functions as needed. There's still lots to do, but this was a big missing piece. It was much harder to get right than I had expected, but I think it's solid! This unfortunately large commit is the result of some wild hacking I've been doing for the past little while. It's the result of a rebase that broke many "wip" commits that tracked my private progress, into something that's not gratuitously messy for our git logs. Since this was a learning and discovery process for me, I've "erased" the confusing git history that wouldn't have helped. I'm happy to discuss the dead-ends, and a small portion of that code was even left in for possible future use. This patch includes: * A change to the cli interface: You now specify the front-end explicitly, instead of leaving it up to the front-end to decide when to "activate". For example, instead of: mgmt run --lang code.mcl we now do: mgmt run lang --lang code.mcl We might rename the --lang flag in the future to avoid the awkward word repetition. Suggestions welcome, but I'm considering "input". One side-effect of this change, is that flags which are "engine" specific now must be specified with "run" before the front-end name. Eg: mgmt run --tmp-prefix lang --lang code.mcl instead of putting --tmp-prefix at the end. We also changed the GAPI slightly, but I've patched all code that used it. This also makes things consistent with the "deploy" command. * The deploys are more robust and let you deploy after a run This has been vastly improved and let's mgmt really run as a smart engine that can handle different workloads. If you don't want to deploy when you've started with `run` or if one comes in, you can use the --no-watch-deploy option to block new deploys. * The import statement exists and works! We now have a working `import` statement. Read the docs, and try it out. I think it's quite elegant how it fits in with `SetScope`. Have a look. As a result, we now have some built-in functions available in modules. This also adds the metadata.yaml entry-point for all modules. Have a look at the examples or the tests. The bulk of the patch is to support this. * Improved lang input parsing code: I re-wrote the parsing that determined what ran when we passed different things to --lang. Deciding between running an mcl file or raw code is now handled in a more intelligent, and re-usable way. See the inputs.go file if you want to have a look. One casualty is that you can't stream code from stdin *directly* to the front-end, it's encapsulated into a deploy first. You can still use stdin though! I doubt anyone will notice this change. * The scope was extended to include functions and classes: Go forth and import lovely code. All these exist in scopes now, and can be re-used! * Function calls actually use the scope now. Glad I got this sorted out. * There is import cycle detection for modules! Yes, this is another dag. I think that's #4. I guess they're useful. * A ton of tests and new test infra was added! This should make it much easier to add new tests that run mcl code. Have a look at TestAstFunc1 to see how to add more of these. As usual, I'll try to keep these commits smaller in the future!
2381 lines
46 KiB
Go
2381 lines
46 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
// +build !root
|
|
|
|
package lang
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
|
"github.com/purpleidea/mgmt/lang/types"
|
|
"github.com/purpleidea/mgmt/util"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/kylelemons/godebug/pretty"
|
|
)
|
|
|
|
func TestLexParse0(t *testing.T) {
|
|
type test struct { // an individual test
|
|
name string
|
|
code string
|
|
fail bool
|
|
exp interfaces.Stmt
|
|
}
|
|
testCases := []test{}
|
|
|
|
{
|
|
testCases = append(testCases, test{
|
|
"nil",
|
|
``,
|
|
false,
|
|
nil,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple assignment",
|
|
code: `$rewsna = -42`,
|
|
fail: false,
|
|
exp: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "rewsna",
|
|
Value: &ExprInt{
|
|
V: -42,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "one res",
|
|
code: `noop "n1" {}`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "res with keyword",
|
|
code: `false "n1" {}`, // false is a special keyword
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "bad escaping",
|
|
code: `
|
|
test "t1" {
|
|
str => "he\ llo", # incorrect escaping
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "int overflow",
|
|
code: `
|
|
test "t1" {
|
|
int => 888888888888888888888888, # overflows
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "overflow after lexer",
|
|
code: `
|
|
test "t1" {
|
|
uint8 => 128, # does not overflow at lexer stage
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "one res with param",
|
|
code: `
|
|
test "t1" {
|
|
int16 => 01134, # some comment
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "one res with elvis",
|
|
code: `
|
|
test "t1" {
|
|
int16 => true ?: 42, # elvis operator
|
|
int32 => 42,
|
|
stringptr => false ?: "", # missing is not ""
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
// TODO: skip trailing comma requirement on one-liners
|
|
testCases = append(testCases, test{
|
|
name: "two lists",
|
|
code: `
|
|
$somelist = [42, 0, -13,]
|
|
$somelonglist = [
|
|
"hello",
|
|
"and",
|
|
"how",
|
|
"are",
|
|
"you?",
|
|
]
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "one map",
|
|
code: `
|
|
$somemap = {
|
|
"foo" => "foo1",
|
|
"bar" => "bar1",
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "another map",
|
|
code: `
|
|
$somemap = {
|
|
"foo" => -13,
|
|
"bar" => 42,
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
// TODO: alternate possible syntax ?
|
|
//{
|
|
// testCases = append(testCases, test{ // ?
|
|
// code: `
|
|
// $somestruct = struct{
|
|
// foo: "foo1";
|
|
// bar: 42 # no trailing semicolon at the moment
|
|
// }
|
|
// `,
|
|
// fail: false,
|
|
// //exp: ???, // FIXME: add the expected AST
|
|
// })
|
|
//}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "one struct",
|
|
code: `
|
|
$somestruct = struct{
|
|
foo => "foo1",
|
|
bar => 42,
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "struct with nested struct",
|
|
code: `
|
|
$somestruct = struct{
|
|
foo => "foo1",
|
|
bar => struct{
|
|
a => true,
|
|
b => "hello",
|
|
},
|
|
baz => 42,
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
// types
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "some lists",
|
|
code: `
|
|
$intlist []int = [42, -0, 13,]
|
|
$intlistnested [][]int = [[42,], [], [100, -0,], [-13,],]
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "maps 1",
|
|
code: `
|
|
# make sure the "str:" part doesn't match a single ident
|
|
$strmap map{str: int} = {
|
|
"key1" => 42,
|
|
"key2" => -13,
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "maps 2",
|
|
code: `
|
|
$mapstrintlist map{str: []int} = {
|
|
"key1" => [42, 44,],
|
|
"key2" => [],
|
|
"key3" => [-13,],
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "maps and lists",
|
|
code: `
|
|
$strmap map{str: int} = {
|
|
"key1" => 42,
|
|
"key2" => -13,
|
|
}
|
|
$mapstrintlist map{str: []int} = {
|
|
"key1" => [42, 44,],
|
|
"key2" => [],
|
|
"key3" => [-13,],
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "some structs",
|
|
code: `
|
|
$structx struct{a int; b bool; c str} = struct{
|
|
a => 42,
|
|
b => true,
|
|
c => "hello",
|
|
}
|
|
$structx2 struct{a int; b []bool; c str} = struct{
|
|
a => 42,
|
|
b => [true, false, false, true,],
|
|
c => "hello",
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "res with floats",
|
|
code: `
|
|
test "t1" {
|
|
float32 => -25.38789, # some float
|
|
float64 => 53.393908945, # some float
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
// FIXME: why doesn't this overflow, and thus fail?
|
|
// from the docs: If s is syntactically well-formed but is more than 1/2
|
|
// ULP away from the largest floating point number of the given size,
|
|
// ParseFloat returns f = ±Inf, err.Err = ErrRange.
|
|
//{
|
|
// testCases = append(testCases, test{
|
|
// name: "overflowing float",
|
|
// code: `
|
|
// test "t1" {
|
|
// float32 => -457643875645764387564578645457864525457643875645764387564578645457864525.457643875645764387564578645457864525387899898753459879587574928798759863965, # overflow
|
|
// }
|
|
// `,
|
|
// fail: true,
|
|
// })
|
|
//}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "res and addition",
|
|
code: `
|
|
test "t1" {
|
|
float32 => -25.38789 + 32.6,
|
|
}
|
|
`,
|
|
fail: false,
|
|
//exp: ???, // FIXME: add the expected AST
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "x1",
|
|
Value: &ExprCall{
|
|
Name: "foo1",
|
|
Args: []interfaces.Expr{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "func call 1",
|
|
code: `
|
|
$x1 = foo1()
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "x1",
|
|
Value: &ExprCall{
|
|
Name: "foo1",
|
|
Args: []interfaces.Expr{
|
|
&ExprInt{
|
|
V: 13,
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "func call 2",
|
|
code: `
|
|
$x1 = foo1(13, "hello")
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "x1",
|
|
Value: &ExprCall{
|
|
Name: "pkg.foo1",
|
|
Args: []interfaces.Expr{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "func call dotted 1",
|
|
code: `
|
|
$x1 = pkg.foo1()
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "x1",
|
|
Value: &ExprCall{
|
|
Name: "pkg.foo1",
|
|
Args: []interfaces.Expr{
|
|
&ExprBool{
|
|
V: true,
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "func call dotted 2",
|
|
code: `
|
|
$x1 = pkg.foo1(true, "hello")
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "func call dotted invalid 1",
|
|
code: `
|
|
$x1 = .pkg.foo1(true, "hello")
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "func call dotted invalid 2",
|
|
code: `
|
|
$x1 = pkg.foo1.(true, "hello")
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "func call dotted invalid 3",
|
|
code: `
|
|
$x1 = .pkg.foo1.(true, "hello")
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "func call dotted invalid 4",
|
|
code: `
|
|
$x1 = pkg..foo1(true, "hello")
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "x1",
|
|
Value: &ExprVar{
|
|
Name: "pkg.foo1",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "dotted var 1",
|
|
code: `
|
|
$x1 = $pkg.foo1
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "x1",
|
|
Value: &ExprVar{
|
|
Name: "pkg.foo1.bar",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "dotted var 2",
|
|
code: `
|
|
$x1 = $pkg.foo1.bar
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "invalid dotted var 1",
|
|
code: `
|
|
$x1 = $.pkg.foo1.bar
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "invalid dotted var 2",
|
|
code: `
|
|
$x1 = $pkg.foo1.bar.
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "invalid dotted var 3",
|
|
code: `
|
|
$x1 = $.pkg.foo1.bar.
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "invalid dotted var 4",
|
|
code: `
|
|
$x1 = $pkg..foo1.bar
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "int64ptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprInt{
|
|
V: 13,
|
|
},
|
|
&ExprInt{
|
|
V: 42,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "addition",
|
|
code: `
|
|
test "t1" {
|
|
int64ptr => 13 + 42,
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "float32",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprFloat{
|
|
V: -25.38789,
|
|
},
|
|
&ExprFloat{
|
|
V: 32.6,
|
|
},
|
|
},
|
|
},
|
|
&ExprFloat{
|
|
V: 13.7,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "multiple float addition",
|
|
code: `
|
|
test "t1" {
|
|
float32 => -25.38789 + 32.6 + 13.7,
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "int64ptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "*",
|
|
},
|
|
&ExprInt{
|
|
V: 3,
|
|
},
|
|
&ExprInt{
|
|
V: 12,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations lucky",
|
|
code: `
|
|
test "t1" {
|
|
int64ptr => 4 + 3 * 12, # 40, not 84
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "int64ptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "*",
|
|
},
|
|
&ExprInt{
|
|
V: 3,
|
|
},
|
|
&ExprInt{
|
|
V: 12,
|
|
},
|
|
},
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations needs left precedence",
|
|
code: `
|
|
test "t1" {
|
|
int64ptr => 3 * 12 + 4, # 40, not 48
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "int64ptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "*",
|
|
},
|
|
&ExprInt{
|
|
V: 3,
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprInt{
|
|
V: 12,
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations parens",
|
|
code: `
|
|
test "t1" {
|
|
int64ptr => 3 * (12 + 4), # 48, not 40
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "boolptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: ">",
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprInt{
|
|
V: 3,
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
},
|
|
},
|
|
&ExprInt{
|
|
V: 5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations bools",
|
|
code: `
|
|
test "t1" {
|
|
boolptr => 3 + 4 > 5, # should be true
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "boolptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: ">",
|
|
},
|
|
&ExprInt{
|
|
V: 3,
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
&ExprInt{
|
|
V: 5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations bools reversed",
|
|
code: `
|
|
test "t1" {
|
|
boolptr => 3 > 4 + 5, # should be false
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "boolptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: ">",
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "!",
|
|
},
|
|
&ExprInt{
|
|
V: 3,
|
|
},
|
|
},
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations with not",
|
|
code: `
|
|
test "t1" {
|
|
boolptr => ! 3 > 4, # should parse, but not compile
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "boolptr",
|
|
Value: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "&&",
|
|
},
|
|
&ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "<",
|
|
},
|
|
&ExprInt{
|
|
V: 7,
|
|
},
|
|
&ExprInt{
|
|
V: 4,
|
|
},
|
|
},
|
|
},
|
|
&ExprBool{
|
|
V: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "order of operations logical",
|
|
code: `
|
|
test "t1" {
|
|
boolptr => 7 < 4 && true, # should be false
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "int64ptr",
|
|
Value: &ExprInt{
|
|
V: 42,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t2",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "int64ptr",
|
|
Value: &ExprInt{
|
|
V: 13,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtEdge{
|
|
EdgeHalfList: []*StmtEdgeHalf{
|
|
{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
SendRecv: "foosend",
|
|
},
|
|
{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t2",
|
|
},
|
|
SendRecv: "barrecv",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "edge stmt",
|
|
code: `
|
|
test "t1" {
|
|
int64ptr => 42,
|
|
}
|
|
test "t2" {
|
|
int64ptr => 13,
|
|
}
|
|
|
|
Test["t1"].foosend -> Test["t2"].barrecv # send/recv
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "parser set type incompatibility str",
|
|
code: `
|
|
$x int = "hello" # type should be str to work
|
|
test "t1" {
|
|
str => $x,
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "parser set type incompatibility int",
|
|
code: `
|
|
$x int = "hello" # value should be int to work
|
|
test "t1" {
|
|
int => $x,
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "c1",
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "stringptr",
|
|
Value: &ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "c1",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple class 1",
|
|
code: `
|
|
class c1 {
|
|
test "t1" {
|
|
stringptr => "hello",
|
|
}
|
|
}
|
|
include c1
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "c1",
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "stringptr",
|
|
Value: &ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "pkg.c1",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted class 1",
|
|
code: `
|
|
# a dotted identifier only occurs via an imported class
|
|
class c1 {
|
|
test "t1" {
|
|
stringptr => "hello",
|
|
}
|
|
}
|
|
# a dotted identifier is allowed here if it's imported
|
|
include pkg.c1
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "c1",
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprStr{
|
|
V: "t1",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "stringptr",
|
|
Value: &ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "pkg.ns.c1",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted class 2",
|
|
code: `
|
|
# a dotted identifier only occurs via an imported class
|
|
class c1 {
|
|
test "t1" {
|
|
stringptr => "hello",
|
|
}
|
|
}
|
|
# a dotted identifier is allowed here if it's imported
|
|
include pkg.ns.c1
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted invalid class 1",
|
|
code: `
|
|
# a dotted identifier only occurs via an imported class
|
|
class foo.c1 {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted invalid class 2",
|
|
code: `
|
|
# a dotted identifier only occurs via an imported class
|
|
class foo.bar.c1 {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted invalid include 1",
|
|
code: `
|
|
class .foo.c1 {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted invalid include 2",
|
|
code: `
|
|
class foo.c1. {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted invalid include 3",
|
|
code: `
|
|
class .foo.c1. {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple dotted invalid include 4",
|
|
code: `
|
|
class foo..c1 {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "x",
|
|
Args: []*Arg{},
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{},
|
|
},
|
|
},
|
|
&StmtClass{
|
|
Name: "y1",
|
|
Args: []*Arg{},
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "z",
|
|
Args: nil,
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple class with args 0",
|
|
code: `
|
|
class x() {
|
|
}
|
|
class y1() {
|
|
}
|
|
include z
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
testCases = append(testCases, test{
|
|
name: "simple class underscore failure",
|
|
code: `
|
|
class x_() {
|
|
}
|
|
`,
|
|
fail: true,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "c1",
|
|
Args: []*Arg{
|
|
{
|
|
Name: "a",
|
|
//Type: &types.Type{},
|
|
},
|
|
{
|
|
Name: "b",
|
|
//Type: &types.Type{},
|
|
},
|
|
},
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprVar{
|
|
Name: "a",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "stringptr",
|
|
Value: &ExprVar{
|
|
Name: "b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "c1",
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "t1",
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "c1",
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "t2",
|
|
},
|
|
&ExprStr{
|
|
V: "world",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple class with args 1",
|
|
code: `
|
|
class c1($a, $b) {
|
|
test $a {
|
|
stringptr => $b,
|
|
}
|
|
}
|
|
include c1("t1", "hello")
|
|
include c1("t2", "world")
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "c1",
|
|
Args: []*Arg{
|
|
{
|
|
Name: "a",
|
|
Type: types.TypeStr,
|
|
},
|
|
{
|
|
Name: "b",
|
|
//Type: &types.Type{},
|
|
},
|
|
},
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtRes{
|
|
Kind: "test",
|
|
Name: &ExprVar{
|
|
Name: "a",
|
|
},
|
|
Contents: []StmtResContents{
|
|
&StmtResField{
|
|
Field: "stringptr",
|
|
Value: &ExprVar{
|
|
Name: "b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "c1",
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "t1",
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "c1",
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "t2",
|
|
},
|
|
&ExprStr{
|
|
V: "world",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple class with typed args 1",
|
|
code: `
|
|
class c1($a str, $b) {
|
|
test $a {
|
|
stringptr => $b,
|
|
}
|
|
}
|
|
include c1("t1", "hello")
|
|
include c1("t2", "world")
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtImport{
|
|
Name: "foo1",
|
|
Alias: "",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple import 1",
|
|
code: `
|
|
import "foo1"
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtImport{
|
|
Name: "foo1",
|
|
Alias: "bar",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple import 2",
|
|
code: `
|
|
import "foo1" as bar
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtImport{
|
|
Name: "foo1",
|
|
Alias: "",
|
|
},
|
|
&StmtImport{
|
|
Name: "foo2",
|
|
Alias: "bar",
|
|
},
|
|
&StmtImport{
|
|
Name: "foo3",
|
|
Alias: "",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple import 3",
|
|
code: `
|
|
import "foo1"
|
|
import "foo2" as bar
|
|
import "foo3"
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtImport{
|
|
Name: "foo1",
|
|
Alias: "*",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple import 4",
|
|
code: `
|
|
import "foo1" as *
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtClass{
|
|
Name: "c1",
|
|
Body: &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtImport{
|
|
Name: "foo",
|
|
Alias: "bar",
|
|
},
|
|
&StmtImport{
|
|
Name: "baz",
|
|
Alias: "",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&StmtInclude{
|
|
Name: "c1",
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple import inside class 1",
|
|
code: `
|
|
class c1 {
|
|
import "foo" as bar
|
|
import "baz"
|
|
}
|
|
include c1
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtFunc{
|
|
Name: "f1",
|
|
Func: &ExprFunc{
|
|
Body: &ExprInt{
|
|
V: 42,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function stmt 1",
|
|
code: `
|
|
func f1() {
|
|
42
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
fn := &ExprFunc{
|
|
Return: types.TypeInt,
|
|
Body: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprInt{
|
|
V: 13,
|
|
},
|
|
&ExprInt{
|
|
V: 42,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// sometimes, the type can get set by the parser when it's known
|
|
if err := fn.SetType(types.NewType("func() int")); err != nil {
|
|
t.Fatal("could not build type")
|
|
}
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtFunc{
|
|
Name: "f2",
|
|
Func: fn,
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function stmt 2",
|
|
code: `
|
|
func f2() int {
|
|
13 + 42
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
fn := &ExprFunc{
|
|
Args: []*Arg{
|
|
{
|
|
Name: "a",
|
|
Type: types.TypeInt,
|
|
},
|
|
{
|
|
Name: "b",
|
|
//Type: &types.Type{},
|
|
},
|
|
},
|
|
Return: types.TypeInt,
|
|
Body: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprVar{
|
|
Name: "a",
|
|
},
|
|
&ExprVar{
|
|
Name: "b",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// we can't set the type here, because it's only partially known
|
|
//if err := fn.SetType(types.NewType("func() int")); err != nil {
|
|
// t.Fatal("could not build type")
|
|
//}
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtFunc{
|
|
Name: "f3",
|
|
Func: fn,
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function stmt 3",
|
|
code: `
|
|
func f3($a int, $b) int {
|
|
$a + $b
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
fn := &ExprFunc{
|
|
Args: []*Arg{
|
|
{
|
|
Name: "x",
|
|
Type: types.TypeStr,
|
|
},
|
|
},
|
|
Return: types.TypeStr,
|
|
Body: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
&ExprVar{
|
|
Name: "x",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := fn.SetType(types.NewType("func(x str) str")); err != nil {
|
|
t.Fatal("could not build type")
|
|
}
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtFunc{
|
|
Name: "f4",
|
|
Func: fn,
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function stmt 4",
|
|
code: `
|
|
func f4($x str) str {
|
|
"hello" + $x
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
|
|
fn := &ExprFunc{
|
|
Body: &ExprInt{
|
|
V: 42,
|
|
},
|
|
}
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "fn",
|
|
Value: fn,
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function expr 1",
|
|
code: `
|
|
# lambda
|
|
$fn = func() {
|
|
42
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
fn := &ExprFunc{
|
|
Args: []*Arg{
|
|
{
|
|
Name: "x",
|
|
Type: types.TypeStr,
|
|
},
|
|
},
|
|
Return: types.TypeStr,
|
|
Body: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
&ExprVar{
|
|
Name: "x",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := fn.SetType(types.NewType("func(x str) str")); err != nil {
|
|
t.Fatal("could not build type")
|
|
}
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "fn",
|
|
Value: fn,
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function expr 2",
|
|
code: `
|
|
# lambda
|
|
$fn = func($x str) str {
|
|
"hello" + $x
|
|
}
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
{
|
|
fn := &ExprFunc{
|
|
Args: []*Arg{
|
|
{
|
|
Name: "x",
|
|
Type: types.TypeStr,
|
|
},
|
|
},
|
|
Return: types.TypeStr,
|
|
Body: &ExprCall{
|
|
Name: operatorFuncName,
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "+",
|
|
},
|
|
&ExprStr{
|
|
V: "hello",
|
|
},
|
|
&ExprVar{
|
|
Name: "x",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := fn.SetType(types.NewType("func(x str) str")); err != nil {
|
|
t.Fatal("could not build type")
|
|
}
|
|
exp := &StmtProg{
|
|
Prog: []interfaces.Stmt{
|
|
&StmtBind{
|
|
Ident: "fn",
|
|
Value: fn,
|
|
},
|
|
&StmtBind{
|
|
Ident: "foo",
|
|
Value: &ExprCall{
|
|
Name: "fn",
|
|
Args: []interfaces.Expr{
|
|
&ExprStr{
|
|
V: "world",
|
|
},
|
|
},
|
|
//Var: true, // XXX: add this!
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases = append(testCases, test{
|
|
name: "simple function expr 3",
|
|
code: `
|
|
# lambda
|
|
$fn = func($x str) str {
|
|
"hello" + $x
|
|
}
|
|
$foo = $fn("world") # helloworld
|
|
`,
|
|
fail: false,
|
|
exp: exp,
|
|
})
|
|
}
|
|
|
|
names := []string{}
|
|
for index, tc := range testCases { // run all the tests
|
|
if tc.name == "" {
|
|
t.Errorf("test #%d: not named", index)
|
|
continue
|
|
}
|
|
if util.StrInList(tc.name, names) {
|
|
t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
|
|
continue
|
|
}
|
|
names = append(names, tc.name)
|
|
|
|
//if index != 3 { // hack to run a subset (useful for debugging)
|
|
//if (index != 20 && index != 21) {
|
|
//if tc.name != "nil" {
|
|
// continue
|
|
//}
|
|
|
|
t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
|
|
name, code, fail, exp := tc.name, tc.code, tc.fail, tc.exp
|
|
|
|
t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name)
|
|
|
|
str := strings.NewReader(code)
|
|
ast, err := LexParse(str)
|
|
|
|
if !fail && err != nil {
|
|
t.Errorf("test #%d: lex/parse failed with: %+v", index, err)
|
|
return
|
|
}
|
|
if fail && err == nil {
|
|
t.Errorf("test #%d: lex/parse passed, expected fail", index)
|
|
return
|
|
}
|
|
|
|
if !fail && ast == nil {
|
|
t.Errorf("test #%d: lex/parse was nil", index)
|
|
return
|
|
}
|
|
|
|
if exp != nil {
|
|
if !reflect.DeepEqual(ast, exp) {
|
|
// double check because DeepEqual is different since the func exists
|
|
diff := pretty.Compare(ast, exp)
|
|
if diff != "" { // bonus
|
|
t.Errorf("test #%d: AST did not match expected", index)
|
|
// TODO: consider making our own recursive print function
|
|
t.Logf("test #%d: actual: \n\n%s\n", index, spew.Sdump(ast))
|
|
t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(exp))
|
|
|
|
// more details, for tricky cases:
|
|
diffable := &pretty.Config{
|
|
Diffable: true,
|
|
IncludeUnexported: true,
|
|
//PrintStringers: false,
|
|
//PrintTextMarshalers: false,
|
|
//SkipZeroFields: false,
|
|
}
|
|
t.Logf("test #%d: actual: \n\n%s\n", index, diffable.Sprint(ast))
|
|
t.Logf("test #%d: expected: \n\n%s", index, diffable.Sprint(exp))
|
|
t.Logf("test #%d: diff:\n%s", index, diff)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLexParse1(t *testing.T) {
|
|
code := `
|
|
$a = 42
|
|
$b = true
|
|
$c = 13
|
|
$d = "hello"
|
|
$e = true
|
|
$f = 3.13
|
|
# some noop resource
|
|
noop "n0" {
|
|
foo => true,
|
|
bar => false # this should be a parser error (no comma)
|
|
}
|
|
# hello
|
|
# world
|
|
test "t1" {}
|
|
` // error
|
|
str := strings.NewReader(code)
|
|
_, err := LexParse(str)
|
|
if e, ok := err.(*LexParseErr); ok && e.Err != ErrParseExpectingComma {
|
|
t.Errorf("lex/parse failure, got: %+v", e)
|
|
} else if err == nil {
|
|
t.Errorf("lex/parse success, expected error")
|
|
} else {
|
|
if e.Row != 10 || e.Col != 9 {
|
|
t.Errorf("expected error at 10 x 9, got: %d x %d", e.Row, e.Col)
|
|
}
|
|
t.Logf("row x col: %d x %d", e.Row, e.Col)
|
|
t.Logf("message: %s", e.Str)
|
|
t.Logf("output: %+v", err)
|
|
}
|
|
}
|
|
|
|
func TestLexParse2(t *testing.T) {
|
|
code := `
|
|
$a == 13
|
|
test "t1" {
|
|
int8 => $a,
|
|
}
|
|
` // error, assignment is a single equals, not two
|
|
str := strings.NewReader(code)
|
|
_, err := LexParse(str)
|
|
if e, ok := err.(*LexParseErr); ok && e.Err != ErrParseAdditionalEquals {
|
|
t.Errorf("lex/parse failure, got: %+v", e)
|
|
} else if err == nil {
|
|
t.Errorf("lex/parse success, expected error")
|
|
} else {
|
|
// TODO: when this is accurate, pick values and enable this!
|
|
//if e.Row != 8 || e.Col != 2 {
|
|
// t.Errorf("expected error at 8 x 2, got: %d x %d", e.Row, e.Col)
|
|
//}
|
|
t.Logf("row x col: %d x %d", e.Row, e.Col)
|
|
t.Logf("message: %s", e.Str)
|
|
t.Logf("output: %+v", err)
|
|
}
|
|
}
|
|
|
|
func TestLexParseWithOffsets1(t *testing.T) {
|
|
code1 := `
|
|
# "file1"
|
|
$a = 42
|
|
$b = true
|
|
$c = 13
|
|
$d = "hello"
|
|
$e = true
|
|
$f = 3.13
|
|
`
|
|
code2 := `
|
|
# "file2"
|
|
# some noop resource
|
|
noop "n0" {
|
|
foo => true,
|
|
bar => false # this should be a parser error (no comma)
|
|
}
|
|
# hello
|
|
# world
|
|
test "t2" {}
|
|
`
|
|
code3 := `
|
|
# "file3"
|
|
# this is some more code
|
|
test "t3" {}
|
|
`
|
|
str1 := strings.NewReader(code1)
|
|
str2 := strings.NewReader(code2)
|
|
str3 := strings.NewReader(code3)
|
|
// TODO: this is currently in number of lines instead of bytes
|
|
o1 := uint64(len(strings.Split(code1, "\n")) - 1)
|
|
o2 := uint64(len(strings.Split(code2, "\n")) - 1)
|
|
//o1 := uint64(len(code1))
|
|
//o2 := uint64(len(code2))
|
|
t.Logf("o1: %+v", o1)
|
|
t.Logf("o2: %+v", o2)
|
|
t.Logf("o1+o2: %+v", o1+o2)
|
|
readers := io.MultiReader(str1, str2, str3)
|
|
offsets := map[uint64]string{
|
|
0: "file1",
|
|
o1: "file2",
|
|
o1 + o2: "file3", // offset is cumulative
|
|
}
|
|
_, err := LexParseWithOffsets(readers, offsets)
|
|
if e, ok := err.(*LexParseErr); ok && e.Err != ErrParseExpectingComma {
|
|
t.Errorf("lex/parse failure, got: %+v", e)
|
|
} else if err == nil {
|
|
t.Errorf("lex/parse success, expected error")
|
|
} else {
|
|
if e.Row != 5 || e.Col != 9 || e.Filename != "file2" {
|
|
t.Errorf("expected error in 'file2' @ 5 x 9, got: '%s' @ %d x %d", e.Filename, e.Row, e.Col)
|
|
}
|
|
t.Logf("file @ row x col: '%s' @ %d x %d", e.Filename, e.Row, e.Col)
|
|
t.Logf("message: %s", e.Str)
|
|
t.Logf("output: %+v", err) // this will be 1-indexed, instead of zero-indexed
|
|
}
|
|
}
|
|
|
|
func TestImportParsing0(t *testing.T) {
|
|
type test struct { // an individual test
|
|
name string
|
|
fail bool
|
|
alias string
|
|
isSystem bool
|
|
isLocal bool
|
|
isFile bool
|
|
path string
|
|
url string
|
|
}
|
|
testCases := []test{}
|
|
testCases = append(testCases, test{ // index: 0
|
|
name: "",
|
|
fail: true, // can't be empty
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "/",
|
|
fail: true, // can't be root
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/mgmt",
|
|
alias: "mgmt",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/mgmt/",
|
|
url: "git://example.com/purpleidea/mgmt",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/mgmt/",
|
|
alias: "mgmt",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/mgmt/",
|
|
url: "git://example.com/purpleidea/mgmt/",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/mgmt/foo/bar/",
|
|
alias: "bar",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/mgmt/foo/bar/",
|
|
// TODO: change this to be more clever about the clone URL
|
|
//url: "git://example.com/purpleidea/mgmt/",
|
|
// TODO: also consider changing `git` to `https` ?
|
|
url: "git://example.com/purpleidea/mgmt/foo/bar/",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/mgmt-foo",
|
|
alias: "foo", // prefix is magic
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/mgmt-foo/",
|
|
url: "git://example.com/purpleidea/mgmt-foo",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/foo-bar",
|
|
alias: "foo_bar",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/foo-bar/",
|
|
url: "git://example.com/purpleidea/foo-bar",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/FOO-bar",
|
|
alias: "foo_bar",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/FOO-bar/",
|
|
url: "git://example.com/purpleidea/FOO-bar",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/foo-BAR",
|
|
alias: "foo_bar",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/foo-BAR/",
|
|
url: "git://example.com/purpleidea/foo-BAR",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/foo-BAR-baz",
|
|
alias: "foo_bar_baz",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/foo-BAR-baz/",
|
|
url: "git://example.com/purpleidea/foo-BAR-baz",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/Module-Name",
|
|
alias: "module_name",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/Module-Name/",
|
|
url: "git://example.com/purpleidea/Module-Name",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/foo-",
|
|
fail: true, // trailing dash or underscore
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/foo_",
|
|
fail: true, // trailing dash or underscore
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "/var/lib/mgmt",
|
|
alias: "mgmt",
|
|
fail: true, // don't allow absolute paths
|
|
//isLocal: true,
|
|
//path: "/var/lib/mgmt",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "/var/lib/mgmt/",
|
|
alias: "mgmt",
|
|
fail: true, // don't allow absolute paths
|
|
//isLocal: true,
|
|
//path: "/var/lib/mgmt/",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/Module-Name?foo=bar&baz=42",
|
|
alias: "module_name",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/Module-Name/",
|
|
url: "git://example.com/purpleidea/Module-Name",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/Module-Name/?foo=bar&baz=42",
|
|
alias: "module_name",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/Module-Name/",
|
|
url: "git://example.com/purpleidea/Module-Name/",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/Module-Name/?sha1=25ad05cce36d55ce1c55fd7e70a3ab74e321b66e",
|
|
alias: "module_name",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/Module-Name/",
|
|
url: "git://example.com/purpleidea/Module-Name/",
|
|
// TODO: report the query string info as an additional param
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git://example.com/purpleidea/Module-Name/subpath/foo",
|
|
alias: "foo",
|
|
isLocal: false,
|
|
path: "example.com/purpleidea/Module-Name/subpath/foo/",
|
|
url: "git://example.com/purpleidea/Module-Name/subpath/foo",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "foo/",
|
|
alias: "foo",
|
|
isLocal: true,
|
|
path: "foo/",
|
|
})
|
|
testCases = append(testCases, test{
|
|
// import foo.mcl # import a file next to me
|
|
name: "foo.mcl",
|
|
alias: "foo",
|
|
isSystem: false,
|
|
isLocal: true,
|
|
isFile: true,
|
|
path: "foo.mcl",
|
|
})
|
|
testCases = append(testCases, test{
|
|
// import server/foo.mcl # import a file in a dir next to me
|
|
name: "server/foo.mcl",
|
|
alias: "foo",
|
|
isSystem: false,
|
|
isLocal: true,
|
|
isFile: true,
|
|
path: "server/foo.mcl",
|
|
})
|
|
testCases = append(testCases, test{
|
|
// import a deeper file (not necessarily a good idea)
|
|
name: "server/vars/blah.mcl",
|
|
alias: "blah",
|
|
isSystem: false,
|
|
isLocal: true,
|
|
isFile: true,
|
|
path: "server/vars/blah.mcl",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "foo/bar",
|
|
alias: "bar",
|
|
isSystem: true, // system because not a dir (no trailing slash)
|
|
isLocal: true, // not really used, but this is what we return
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "foo/bar/baz",
|
|
alias: "baz",
|
|
isSystem: true, // system because not a dir (no trailing slash)
|
|
isLocal: true, // not really used, but this is what we return
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "fmt",
|
|
alias: "fmt",
|
|
isSystem: true,
|
|
isLocal: true, // not really used, but this is what we return
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "blah",
|
|
alias: "blah",
|
|
isSystem: true, // even modules that don't exist return true here
|
|
isLocal: true,
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git:///home/james/code/mgmt-example1/",
|
|
alias: "example1",
|
|
isSystem: false,
|
|
isLocal: false,
|
|
// FIXME: do we want to have a special "local" imports dir?
|
|
path: "home/james/code/mgmt-example1/",
|
|
url: "git:///home/james/code/mgmt-example1/",
|
|
})
|
|
testCases = append(testCases, test{
|
|
name: "git:////home/james/code/mgmt-example1/",
|
|
fail: true, // don't allow double root slash
|
|
})
|
|
|
|
t.Logf("ModuleMagicPrefix: %s", ModuleMagicPrefix)
|
|
names := []string{}
|
|
for index, tc := range testCases { // run all the tests
|
|
if util.StrInList(tc.name, names) {
|
|
t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
|
|
continue
|
|
}
|
|
names = append(names, tc.name)
|
|
t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
|
|
name, fail, alias, isSystem, isLocal, isFile, path, url := tc.name, tc.fail, tc.alias, tc.isSystem, tc.isLocal, tc.isFile, tc.path, tc.url
|
|
|
|
output, err := ParseImportName(name)
|
|
if !fail && err != nil {
|
|
t.Errorf("test #%d: FAIL", index)
|
|
t.Errorf("test #%d: ParseImportName failed with: %+v", index, err)
|
|
return
|
|
}
|
|
if fail && err == nil {
|
|
t.Errorf("test #%d: FAIL", index)
|
|
t.Errorf("test #%d: ParseImportName expected error, not nil", index)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
return
|
|
}
|
|
if fail { // we failed as expected, don't continue...
|
|
return
|
|
}
|
|
|
|
if alias != output.Alias {
|
|
t.Errorf("test #%d: unexpected value for: `Alias`", index)
|
|
//t.Logf("test #%d: input: %s", index, name)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
t.Logf("test #%d: alias: %s", index, alias)
|
|
return
|
|
}
|
|
if isSystem != output.IsSystem {
|
|
t.Errorf("test #%d: unexpected value for: `IsSystem`", index)
|
|
//t.Logf("test #%d: input: %s", index, name)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
t.Logf("test #%d: isSystem: %t", index, isSystem)
|
|
return
|
|
}
|
|
if isLocal != output.IsLocal {
|
|
t.Errorf("test #%d: unexpected value for: `IsLocal`", index)
|
|
//t.Logf("test #%d: input: %s", index, name)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
t.Logf("test #%d: isLocal: %t", index, isLocal)
|
|
return
|
|
}
|
|
if isFile != output.IsFile {
|
|
t.Errorf("test #%d: unexpected value for: `isFile`", index)
|
|
//t.Logf("test #%d: input: %s", index, name)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
t.Logf("test #%d: isFile: %t", index, isFile)
|
|
return
|
|
}
|
|
if path != output.Path {
|
|
t.Errorf("test #%d: unexpected value for: `Path`", index)
|
|
//t.Logf("test #%d: input: %s", index, name)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
t.Logf("test #%d: path: %s", index, path)
|
|
return
|
|
}
|
|
if url != output.URL {
|
|
t.Errorf("test #%d: unexpected value for: `URL`", index)
|
|
//t.Logf("test #%d: input: %s", index, name)
|
|
t.Logf("test #%d: output: %+v", index, output)
|
|
t.Logf("test #%d: url: %s", index, url)
|
|
return
|
|
}
|
|
|
|
// add some additional sanity checking:
|
|
if strings.HasPrefix(path, "/") {
|
|
t.Errorf("test #%d: the path value starts with a / (it should be relative)", index)
|
|
}
|
|
if !isSystem {
|
|
if !strings.HasSuffix(path, "/") && !strings.HasSuffix(path, interfaces.DotFileNameExtension) {
|
|
t.Errorf("test #%d: the path value should be a directory or a code file", index)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|