lang: Structurally refactor type unification
This will make it easier to add new solvers and also cleans up some pending issues.
This commit is contained in:
352
lang/unification/solvers/simplesolver_test.go
Normal file
352
lang/unification/solvers/simplesolver_test.go
Normal file
@@ -0,0 +1,352 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2024+ 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 <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
//go:build !root
|
||||
|
||||
package solvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/purpleidea/mgmt/lang/ast"
|
||||
"github.com/purpleidea/mgmt/lang/interfaces"
|
||||
"github.com/purpleidea/mgmt/lang/types"
|
||||
"github.com/purpleidea/mgmt/lang/unification"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
)
|
||||
|
||||
func TestSimpleSolver1(t *testing.T) {
|
||||
type test struct { // an individual test
|
||||
name string
|
||||
invariants []interfaces.Invariant
|
||||
expected []interfaces.Expr
|
||||
fail bool
|
||||
expect map[interfaces.Expr]*types.Type
|
||||
experr error // expected error if fail == true (nil ignores it)
|
||||
experrstr string // expected error prefix
|
||||
}
|
||||
testCases := []test{}
|
||||
|
||||
{
|
||||
expr := &ast.ExprStr{V: "hello"}
|
||||
|
||||
invariants := []interfaces.Invariant{
|
||||
&interfaces.EqualsInvariant{
|
||||
Expr: expr,
|
||||
Type: types.NewType("str"),
|
||||
},
|
||||
}
|
||||
|
||||
invars, err := expr.Unify()
|
||||
if err != nil {
|
||||
panic("bad test")
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "simple str",
|
||||
invariants: invariants,
|
||||
expected: []interfaces.Expr{
|
||||
expr,
|
||||
},
|
||||
fail: false,
|
||||
expect: map[interfaces.Expr]*types.Type{
|
||||
expr: types.TypeStr,
|
||||
},
|
||||
})
|
||||
}
|
||||
{
|
||||
expr := &ast.ExprStr{V: "hello"}
|
||||
|
||||
invariants := []interfaces.Invariant{
|
||||
&interfaces.EqualsInvariant{
|
||||
Expr: expr,
|
||||
Type: types.NewType("int"),
|
||||
},
|
||||
}
|
||||
|
||||
invars, err := expr.Unify()
|
||||
if err != nil {
|
||||
panic("bad test")
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "simple fail",
|
||||
invariants: invariants,
|
||||
expected: []interfaces.Expr{
|
||||
expr,
|
||||
},
|
||||
fail: true,
|
||||
//experr: ErrAmbiguous,
|
||||
})
|
||||
}
|
||||
{
|
||||
// ?1 = func(x ?2) ?3
|
||||
// ?1 = func(arg0 str) ?4
|
||||
// ?3 = str # needed since we don't know what the func body is
|
||||
expr1 := &interfaces.ExprAny{} // ?1
|
||||
expr2 := &interfaces.ExprAny{} // ?2
|
||||
expr3 := &interfaces.ExprAny{} // ?3
|
||||
expr4 := &interfaces.ExprAny{} // ?4
|
||||
|
||||
arg0 := &interfaces.ExprAny{} // arg0
|
||||
|
||||
invarA := &interfaces.EqualityWrapFuncInvariant{
|
||||
Expr1: expr1, // Expr
|
||||
Expr2Map: map[string]interfaces.Expr{ // map[string]Expr
|
||||
"x": expr2,
|
||||
},
|
||||
Expr2Ord: []string{"x"}, // []string
|
||||
Expr2Out: expr3, // Expr
|
||||
}
|
||||
|
||||
invarB := &interfaces.EqualityWrapFuncInvariant{
|
||||
Expr1: expr1, // Expr
|
||||
Expr2Map: map[string]interfaces.Expr{ // map[string]Expr
|
||||
"arg0": arg0,
|
||||
},
|
||||
Expr2Ord: []string{"arg0"}, // []string
|
||||
Expr2Out: expr4, // Expr
|
||||
}
|
||||
|
||||
invarC := &interfaces.EqualsInvariant{
|
||||
Expr: expr3,
|
||||
Type: types.NewType("str"),
|
||||
}
|
||||
|
||||
invarD := &interfaces.EqualsInvariant{
|
||||
Expr: arg0,
|
||||
Type: types.NewType("str"),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "dual functions",
|
||||
invariants: []interfaces.Invariant{
|
||||
invarA,
|
||||
invarB,
|
||||
invarC,
|
||||
invarD,
|
||||
},
|
||||
expected: []interfaces.Expr{
|
||||
expr1,
|
||||
expr2,
|
||||
expr3,
|
||||
expr4,
|
||||
arg0,
|
||||
},
|
||||
fail: false,
|
||||
expect: map[interfaces.Expr]*types.Type{
|
||||
expr1: types.NewType("func(str) str"),
|
||||
expr2: types.NewType("str"),
|
||||
expr3: types.NewType("str"),
|
||||
expr4: types.NewType("str"),
|
||||
arg0: types.NewType("str"),
|
||||
},
|
||||
})
|
||||
}
|
||||
{
|
||||
// even though the arg names are different, it still unifies!
|
||||
// ?1 = func(x str) ?2
|
||||
// ?1 = func(y str) ?3
|
||||
// ?3 = str # needed since we don't know what the func body is
|
||||
expr1 := &interfaces.ExprAny{} // ?1
|
||||
expr2 := &interfaces.ExprAny{} // ?2
|
||||
expr3 := &interfaces.ExprAny{} // ?3
|
||||
|
||||
argx := &interfaces.ExprAny{} // argx
|
||||
argy := &interfaces.ExprAny{} // argy
|
||||
|
||||
invarA := &interfaces.EqualityWrapFuncInvariant{
|
||||
Expr1: expr1, // Expr
|
||||
Expr2Map: map[string]interfaces.Expr{ // map[string]Expr
|
||||
"x": argx,
|
||||
},
|
||||
Expr2Ord: []string{"x"}, // []string
|
||||
Expr2Out: expr2, // Expr
|
||||
}
|
||||
|
||||
invarB := &interfaces.EqualityWrapFuncInvariant{
|
||||
Expr1: expr1, // Expr
|
||||
Expr2Map: map[string]interfaces.Expr{ // map[string]Expr
|
||||
"y": argy,
|
||||
},
|
||||
Expr2Ord: []string{"y"}, // []string
|
||||
Expr2Out: expr3, // Expr
|
||||
}
|
||||
|
||||
invarC := &interfaces.EqualsInvariant{
|
||||
Expr: expr3,
|
||||
Type: types.NewType("str"),
|
||||
}
|
||||
|
||||
invarD := &interfaces.EqualsInvariant{
|
||||
Expr: argx,
|
||||
Type: types.NewType("str"),
|
||||
}
|
||||
|
||||
invarE := &interfaces.EqualsInvariant{
|
||||
Expr: argy,
|
||||
Type: types.NewType("str"),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "different func arg names",
|
||||
invariants: []interfaces.Invariant{
|
||||
invarA,
|
||||
invarB,
|
||||
invarC,
|
||||
invarD,
|
||||
invarE,
|
||||
},
|
||||
expected: []interfaces.Expr{
|
||||
expr1,
|
||||
expr2,
|
||||
expr3,
|
||||
argx,
|
||||
argy,
|
||||
},
|
||||
fail: false,
|
||||
expect: map[interfaces.Expr]*types.Type{
|
||||
expr1: types.NewType("func(str) str"),
|
||||
expr2: types.NewType("str"),
|
||||
expr3: types.NewType("str"),
|
||||
argx: types.NewType("str"),
|
||||
argy: types.NewType("str"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
|
||||
invariants, expected, fail, expect, experr, experrstr := tc.invariants, tc.expected, tc.fail, tc.expect, tc.experr, tc.experrstr
|
||||
|
||||
debug := testing.Verbose()
|
||||
logf := func(format string, v ...interface{}) {
|
||||
t.Logf(fmt.Sprintf("test #%d", index)+": "+format, v...)
|
||||
}
|
||||
|
||||
solver, err := unification.LookupDefault()
|
||||
if err != nil {
|
||||
t.Errorf("test #%d: FAIL", index)
|
||||
t.Errorf("test #%d: solver lookup failed with: %+v", index, err)
|
||||
return
|
||||
}
|
||||
init := &unification.Init{
|
||||
Debug: debug,
|
||||
Logf: logf,
|
||||
}
|
||||
if err := solver.Init(init); err != nil {
|
||||
t.Errorf("test #%d: FAIL", index)
|
||||
t.Errorf("test #%d: solver init failed with: %+v", index, err)
|
||||
return
|
||||
}
|
||||
solution, err := solver.Solve(context.TODO(), invariants, expected)
|
||||
t.Logf("test #%d: solver completed with: %+v", index, err)
|
||||
|
||||
if !fail && err != nil {
|
||||
t.Errorf("test #%d: FAIL", index)
|
||||
t.Errorf("test #%d: solver failed with: %+v", index, err)
|
||||
return
|
||||
}
|
||||
if fail && err == nil {
|
||||
t.Errorf("test #%d: FAIL", index)
|
||||
t.Errorf("test #%d: solver passed, expected fail", index)
|
||||
return
|
||||
}
|
||||
if fail && experr != nil && err != experr { // test for specific error!
|
||||
t.Errorf("test #%d: FAIL", index)
|
||||
t.Errorf("test #%d: expected fail, got wrong error", index)
|
||||
t.Errorf("test #%d: got error: %+v", index, err)
|
||||
t.Errorf("test #%d: exp error: %+v", index, experr)
|
||||
return
|
||||
}
|
||||
|
||||
if fail && err != nil {
|
||||
t.Logf("test #%d: err: %+v", index, err)
|
||||
}
|
||||
// test for specific error string!
|
||||
if fail && experrstr != "" && !strings.HasPrefix(err.Error(), experrstr) {
|
||||
t.Errorf("test #%d: FAIL", index)
|
||||
t.Errorf("test #%d: expected fail, got wrong error", index)
|
||||
t.Errorf("test #%d: got error: %s", index, err.Error())
|
||||
t.Errorf("test #%d: exp error: %s", index, experrstr)
|
||||
return
|
||||
}
|
||||
|
||||
if expect == nil { // map[interfaces.Expr]*types.Type
|
||||
return
|
||||
}
|
||||
|
||||
solutions := solution.Solutions
|
||||
if debug {
|
||||
t.Logf("\n\ntest #%d: solutions: %+v\n", index, solutions)
|
||||
}
|
||||
|
||||
solutionsMap := make(map[interfaces.Expr]*types.Type)
|
||||
for _, invar := range solutions {
|
||||
solutionsMap[invar.Expr] = invar.Type
|
||||
}
|
||||
|
||||
var failed bool
|
||||
// TODO: do this in sorted order
|
||||
for expr, exptyp := range expect {
|
||||
typ, exists := solutionsMap[expr]
|
||||
if !exists {
|
||||
t.Errorf("test #%d: solution missing for: %+v", index, expr)
|
||||
failed = true
|
||||
break
|
||||
}
|
||||
if err := exptyp.Cmp(typ); err != nil {
|
||||
t.Errorf("test #%d: solutions type cmp failed with: %+v", index, err)
|
||||
t.Logf("test #%d: got: %+v", index, exptyp)
|
||||
t.Logf("test #%d: exp: %+v", index, typ)
|
||||
failed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user