mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
d3f34a3387
Have done this some time in the past, but there was a few new issues this would highlight now that we're on Go 1.24. Mostly: - Use `b.Loop()` in benchmarks - Use `strings.SplitSeq` where possible - Remove `omitempty` tag for types that can't be empty Signed-off-by: Anders Eknert <anders@eknert.com>
312 lines
6.9 KiB
Go
312 lines
6.9 KiB
Go
package ast
|
|
|
|
import (
|
|
"path"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
)
|
|
|
|
func TestParserCatchesIllegalCapabilities(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
regoVersion RegoVersion
|
|
capabilities Capabilities
|
|
expErr string
|
|
}{
|
|
{
|
|
note: "v0, bad future keyword",
|
|
regoVersion: RegoV0,
|
|
capabilities: Capabilities{
|
|
FutureKeywords: []string{"deadbeef"},
|
|
},
|
|
expErr: "illegal capabilities: unknown keyword: deadbeef",
|
|
},
|
|
{
|
|
note: "v1, bad future keyword",
|
|
regoVersion: RegoV1,
|
|
capabilities: Capabilities{
|
|
Features: []string{FeatureRegoV1},
|
|
FutureKeywords: []string{"deadbeef"},
|
|
},
|
|
expErr: "illegal capabilities: unknown keyword: deadbeef",
|
|
},
|
|
{
|
|
note: "v1, no rego_v1 feature",
|
|
regoVersion: RegoV1,
|
|
capabilities: Capabilities{},
|
|
expErr: "illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var opts ParserOptions
|
|
opts.Capabilities = &tc.capabilities
|
|
|
|
opts.RegoVersion = tc.regoVersion
|
|
|
|
_, _, err := ParseStatementsWithOpts("test.rego", "true", opts)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
} else if errs, ok := err.(Errors); !ok || len(errs) != 1 {
|
|
t.Fatal("expected exactly one error but got:", err)
|
|
} else if errs[0].Code != ParseErr || errs[0].Message != tc.expErr {
|
|
t.Fatal("unexpected error:", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParserCatchesIllegalFutureKeywordsBasedOnCapabilities(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
regoVersion RegoVersion
|
|
}{
|
|
{
|
|
note: "v0",
|
|
regoVersion: RegoV0,
|
|
},
|
|
{
|
|
note: "v1",
|
|
regoVersion: RegoV1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var opts ParserOptions
|
|
opts.Capabilities = CapabilitiesForThisVersion()
|
|
opts.FutureKeywords = []string{"deadbeef"}
|
|
|
|
opts.RegoVersion = tc.regoVersion
|
|
|
|
_, _, err := ParseStatementsWithOpts("test.rego", "true", opts)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
} else if errs, ok := err.(Errors); !ok || len(errs) != 1 {
|
|
t.Fatal("expected exactly one error but got:", err)
|
|
} else if errs[0].Code != ParseErr || errs[0].Message != "unknown future keyword: deadbeef" {
|
|
t.Fatal("unexpected error:", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParserCapabilitiesWithSpecificOptInAndOlderOPA(t *testing.T) {
|
|
|
|
src := `
|
|
package test
|
|
|
|
import future.keywords.in
|
|
|
|
p {
|
|
1 in [3,2,1]
|
|
}
|
|
`
|
|
|
|
opts := ParserOptions{
|
|
Capabilities: &Capabilities{},
|
|
RegoVersion: RegoV0,
|
|
}
|
|
|
|
_, err := ParseModuleWithOpts("test.rego", src, opts)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
} else if errs, ok := err.(Errors); !ok || len(errs) != 1 {
|
|
t.Fatal("expected exactly one error but got:", err)
|
|
} else if errs[0].Code != ParseErr || errs[0].Location.Row != 4 || errs[0].Message != "unexpected keyword, must be one of []" {
|
|
t.Fatal("unexpected error:", err)
|
|
}
|
|
}
|
|
|
|
func TestParserCapabilitiesWithWildcardOptInAndOlderOPA(t *testing.T) {
|
|
|
|
src := `
|
|
package test
|
|
|
|
import future.keywords
|
|
|
|
p {
|
|
1 in [3,2,1]
|
|
}
|
|
`
|
|
opts := ParserOptions{
|
|
Capabilities: &Capabilities{},
|
|
RegoVersion: RegoV0,
|
|
}
|
|
|
|
_, err := ParseModuleWithOpts("test.rego", src, opts)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
} else if errs, ok := err.(Errors); !ok || len(errs) != 1 {
|
|
t.Fatal("expected exactly one error but got:", err)
|
|
} else if errs[0].Code != ParseErr || errs[0].Location.Row != 7 || errs[0].Message != "unexpected identifier token: expected \\n or ; or }" {
|
|
t.Fatal("unexpected error:", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadCapabilitiesVersion(t *testing.T) {
|
|
|
|
capabilitiesVersions, err := LoadCapabilitiesVersions()
|
|
if err != nil {
|
|
t.Fatal("expected success", err)
|
|
}
|
|
|
|
if len(capabilitiesVersions) == 0 {
|
|
t.Fatal("expected a non-empty array of capabilities versions")
|
|
}
|
|
for _, cv := range capabilitiesVersions {
|
|
if _, err := LoadCapabilitiesVersion(cv); err != nil {
|
|
t.Fatal("expected success", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadCapabilitiesFile(t *testing.T) {
|
|
|
|
files := map[string]string{
|
|
"test-capabilities.json": `
|
|
{
|
|
"builtins": []
|
|
}
|
|
`,
|
|
}
|
|
|
|
test.WithTempFS(files, func(root string) {
|
|
_, err := LoadCapabilitiesFile(path.Join(root, "test-capabilities.json"))
|
|
if err != nil {
|
|
t.Fatal("expected success", err)
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
func TestCapabilitiesAddBuiltinSorted(t *testing.T) {
|
|
|
|
c := CapabilitiesForThisVersion()
|
|
|
|
indexOfEq := findBuiltinIndex(c, "eq")
|
|
if indexOfEq < 0 {
|
|
panic("expected to find eq")
|
|
}
|
|
|
|
c.addBuiltinSorted(&Builtin{Name: "eq"})
|
|
|
|
if c.Builtins[indexOfEq].Decl != nil {
|
|
t.Fatal("expected builtin to get overwritten")
|
|
}
|
|
|
|
c.addBuiltinSorted(&Builtin{Name: "~foo"}) // non-existent but always sorts to the end
|
|
|
|
if findBuiltinIndex(c, "~foo") != len(c.Builtins)-1 {
|
|
t.Fatal("expected builtin to be last in slice")
|
|
}
|
|
|
|
c.addBuiltinSorted(&Builtin{Name: " foo"}) // non-existent but always sorts to start
|
|
|
|
if findBuiltinIndex(c, " foo") != 0 {
|
|
t.Fatal("expected builtin to be first in slice")
|
|
}
|
|
|
|
c.addBuiltinSorted(&Builtin{Name: "plus1"}) // non-existent but always after plus in middle
|
|
|
|
if findBuiltinIndex(c, "plus1") != findBuiltinIndex(c, "plus")+1 {
|
|
t.Fatal("expected builtin to be immediately after plus")
|
|
}
|
|
}
|
|
|
|
func TestCapabilitiesMinimumCompatibleVersion(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
version string
|
|
}{
|
|
{
|
|
note: "builtins",
|
|
module: `
|
|
package x
|
|
p { array.reverse([1,2,3]) }
|
|
`,
|
|
version: "0.36.0",
|
|
},
|
|
{
|
|
note: "keywords",
|
|
module: `
|
|
package x
|
|
import future.keywords.every
|
|
`,
|
|
version: "0.38.0",
|
|
},
|
|
{
|
|
note: "features (string prefix ref)",
|
|
module: `
|
|
package x
|
|
import future.keywords.if
|
|
p.a.b.c.d if { true }
|
|
`,
|
|
version: "0.46.0",
|
|
},
|
|
{
|
|
note: "features (general ref)",
|
|
module: `
|
|
package x
|
|
import future.keywords.if
|
|
p.a.b[c].d if { c := "foo" }
|
|
`,
|
|
version: "0.59.0",
|
|
},
|
|
{
|
|
note: "features (general ref + string prefix ref)",
|
|
module: `
|
|
package x
|
|
import future.keywords.if
|
|
p.a.b.c.d if { true }
|
|
p.a.b[c].d if { c := "foo" }
|
|
`,
|
|
version: "0.59.0",
|
|
},
|
|
{
|
|
note: "rego.v1 import",
|
|
module: `
|
|
package x
|
|
import rego.v1`,
|
|
version: "0.59.0",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := MustCompileModulesWithOpts(map[string]string{"test.rego": tc.module}, CompileOpts{
|
|
ParserOptions: ParserOptions{
|
|
RegoVersion: RegoV0,
|
|
},
|
|
})
|
|
minVersion, found := c.Required.MinimumCompatibleVersion()
|
|
if !found || minVersion != tc.version {
|
|
t.Fatal("expected", tc.version, "but got", minVersion)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkCapabilitiesCurrentVersion(b *testing.B) {
|
|
var caps *Capabilities
|
|
for b.Loop() {
|
|
caps = CapabilitiesForThisVersion()
|
|
}
|
|
if caps == nil {
|
|
b.Fatal("expected capabilities to be non-nil")
|
|
}
|
|
}
|
|
|
|
func findBuiltinIndex(c *Capabilities, name string) int {
|
|
for i, bi := range c.Builtins {
|
|
if bi.Name == name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|