pulumi/pkg/eval/intrinsics_impl_test.go
joeduffy 1df1792d84 Ensure all exit paths call OnDone
If certain early errors occurred, like failing to find a default module
or main entrypoint, we never properly invoked OnDone (or, sometimes,
OnStart, for that matter).  This meant that callers like the eval source
in the deployment engine could end up missing signals; in this particular
case, it led to a failure to signal a rendezvous synchronization object,
which itself led to a hang.

The fix is simple: make sure to call the On* methods in the right places.
I've added tests to probe the interesting paths, including failures.

This fixes pulumi/pulumi-fabric#281.
2017-08-02 15:28:48 -07:00

298 lines
10 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package eval
import (
"fmt"
"testing"
"github.com/pulumi/pulumi-fabric/pkg/compiler/ast"
"github.com/pulumi/pulumi-fabric/pkg/compiler/binder"
"github.com/pulumi/pulumi-fabric/pkg/compiler/types"
"github.com/pulumi/pulumi-fabric/pkg/eval/rt"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/stretchr/testify/assert"
)
// intrin carries the qualified name and signature for an intrinsic function
type intrin struct {
moduleMember tokens.ModuleMember
paramTypes []tokens.Type
returnType tokens.Type
}
func makeFakeIntrinsicDefinition(intrin intrin) *ast.Module {
moduleMemberName := intrin.moduleMember.Name()
moduleName := tokens.Name(intrin.moduleMember.Module().Name())
functionName := tokens.Name(intrin.moduleMember.Name())
intrinToken := tokens.Token(intrin.moduleMember)
var params []*ast.LocalVariable
for i, pty := range intrin.paramTypes {
params = append(params, &ast.LocalVariable{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: tokens.Name(fmt.Sprintf("x%d", i)),
},
},
VariableNode: ast.VariableNode{
Type: &ast.TypeToken{
Tok: pty,
},
},
})
}
return &ast.Module{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: moduleName,
},
},
Exports: &ast.ModuleExports{
moduleMemberName: &ast.Export{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: functionName,
},
},
Referent: &ast.Token{
Tok: intrinToken,
},
},
},
Members: &ast.ModuleMembers{
moduleMemberName: &ast.ModuleMethod{
FunctionNode: ast.FunctionNode{
Parameters: &params,
ReturnType: &ast.TypeToken{
Tok: intrin.returnType,
},
Body: &ast.Block{
Statements: []ast.Statement{
&ast.ThrowStatement{
Expression: &ast.StringLiteral{
Value: "unreachable",
},
},
},
},
},
ModuleMemberNode: ast.ModuleMemberNode{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: functionName,
},
},
},
},
},
}
}
// makeInvokeIntrinsicAST creates the AST for invoking a requested intrinsic, potentially dynamically, with a
// provided argument list. It returns the statements to invoke.
func makeInvokeIntrinsicAST(intrin tokens.ModuleMember, dynamic bool, args []ast.Expression) []ast.Statement {
var loadFuncExpr ast.Expression
if dynamic {
var loadLumiMod ast.Expression = &ast.LoadDynamicExpression{
Name: &ast.StringLiteral{
Value: intrin.Module().Name().String(),
},
}
loadFuncExpr = &ast.LoadDynamicExpression{
Object: &loadLumiMod,
Name: &ast.StringLiteral{
Value: intrin.Name().String(),
},
}
} else {
loadFuncExpr = &ast.LoadLocationExpression{
Name: &ast.Token{
Tok: tokens.Token(intrin),
},
}
}
var arguments []*ast.CallArgument
for _, arg := range args {
arguments = append(arguments, &ast.CallArgument{Expr: arg})
}
var invokeExpr ast.Expression = &ast.InvokeFunctionExpression{
CallExpressionNode: ast.CallExpressionNode{
Arguments: &arguments,
},
Function: loadFuncExpr,
}
return []ast.Statement{
&ast.Import{
Referent: &ast.Token{
Tok: tokens.Token(intrin.Module()),
},
Name: &ast.Identifier{
Ident: tokens.Name(intrin.Module().Name().String()),
},
},
&ast.ReturnStatement{
Expression: &invokeExpr,
},
}
}
// invokeIntrinsic creates an AST for calling the intrinsic with the provided arguments and evaluates that AST in a
// fresh evaluator. It returns the resulting object and unwind, as well as the binder used during evaluation. If the
// dynamic flag is true, the function is loaded dynamically, else it is loaded statically through a reference to the
// intrinsic symbol.
func invokeIntrinsic(intrin intrin, dynamic bool, args []ast.Expression) (binder.Binder,
*rt.Object, *rt.Unwind) {
b, e := newTestEval(nil)
body := makeInvokeIntrinsicAST(intrin.moduleMember, dynamic, args)
fakeIntrinsicDefinitionModule := makeFakeIntrinsicDefinition(intrin)
pack := makeTestPackage("lumirt")
addTestDefaultModule(pack, true, body)
(*pack.Modules)[tokens.ModuleName("index")] = fakeIntrinsicDefinitionModule
sym := b.BindPackage(pack)
ret, uw := e.EvaluatePackage(sym, nil)
return b, ret, uw
}
// TestIsFunction verifies the `lumirt:index:isFunction` intrinsic.
func Test_IsFunction(t *testing.T) {
t.Parallel()
isFunctionIntrin := intrin{
moduleMember: tokens.ModuleMember("lumirt:index:isFunction"),
paramTypes: []tokens.Type{types.Object.TypeToken()},
returnType: types.Bool.TypeToken(),
}
aFunction := &ast.LoadLocationExpression{
Name: &ast.Token{
Tok: tokens.Token(isFunctionIntrin.moduleMember),
},
}
notAFunction := &ast.NullLiteral{}
// variant #1: invoke the function statically, passing a null literal; expect a false return.
{
b, ret, uw := invokeIntrinsic(isFunctionIntrin, false, []ast.Expression{notAFunction})
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Unexpected return type: %v", ret.Type())
val := ret.BoolValue()
assert.Equal(t, false, val, "Unexpected return value: %v", val)
}
// variant #2: invoke the function dynamically, passing a null literal; expect a false return.
{
b, ret, uw := invokeIntrinsic(isFunctionIntrin, true, []ast.Expression{notAFunction})
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Unexpected return type: %v", ret.Type())
val := ret.BoolValue()
assert.Equal(t, false, val, "Unexpected return value: %v", val)
}
// variant #3: invoke the function statically, passing a real function; expect a true return.
{
b, ret, uw := invokeIntrinsic(isFunctionIntrin, false, []ast.Expression{aFunction})
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Unexpected return type: %v", ret.Type())
val := ret.BoolValue()
assert.Equal(t, true, val, "Unexpected return value: %v", val)
}
// variant #4: invoke the function dynamically, passing a real function; expect a true return.
{
b, ret, uw := invokeIntrinsic(isFunctionIntrin, true, []ast.Expression{aFunction})
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsBool(), "Unexpected return type: %v", ret.Type())
val := ret.BoolValue()
assert.Equal(t, true, val, "Unexpected return value: %v", val)
}
}
func Test_JsonStringify(t *testing.T) {
t.Parallel()
jsonStringifyIntrin := intrin{
moduleMember: tokens.ModuleMember("lumirt:index:jsonStringify"),
paramTypes: []tokens.Type{types.Object.TypeToken()},
returnType: types.String.TypeToken(),
}
{
//jsonStringify(`a
//b"`)
obj := &ast.StringLiteral{
Value: `a
b"`,
}
b, ret, uw := invokeIntrinsic(jsonStringifyIntrin, true, []ast.Expression{obj})
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
assert.True(t, ret.IsString(), "Unexpected return type: %v", ret.Type())
val := ret.StringValue()
assert.Equal(t, "\"a\\nb\\\"\"", val, "Unexpected return value: %v", val)
}
}
func Test_SerializeClosure(t *testing.T) {
t.Parallel()
serializeClosure := intrin{
moduleMember: tokens.ModuleMember("lumirt:index:serializeClosure"),
paramTypes: []tokens.Type{types.Dynamic.TypeToken()},
returnType: types.Dynamic.TypeToken(),
}
{
// let f = () => 12
f := &ast.LambdaExpression{
FunctionNode: ast.FunctionNode{
Body: &ast.ExpressionStatement{
Expression: &ast.NumberLiteral{
Value: 12.0,
},
},
Parameters: &[]*ast.LocalVariable{},
ReturnType: &ast.TypeToken{
Tok: types.Number.TypeToken(),
},
},
SourceLanguage: ".js",
SourceText: "return function() { return 12; }",
}
b, ret, uw := invokeIntrinsic(serializeClosure, true, []ast.Expression{f})
assert.True(t, b.Diag().Success(), "Expected a successful evaluation")
assert.Nil(t, uw, "Did not expect a out-of-the-ordinary unwind to occur (expected a return)")
assert.NotNil(t, ret, "Expected a non-nil return value")
code := ret.GetPropertyAddr("code", false, true)
assert.NotNil(t, code, "Expected a non-nil 'code' property")
assert.True(t, code.Obj().IsString(), "Expected 'code' to be a string")
assert.Equal(t, "return function() { return 12; }", code.Obj().StringValue(), "Expected 'code' to be a string")
signature := ret.GetPropertyAddr("signature", false, true)
assert.NotNil(t, signature, "Expected a non-nil 'signature' property")
assert.True(t, signature.Obj().IsString(), "Expected 'signature' to be a string")
assert.Equal(t, "()number",
signature.Obj().StringValue(), "Expected 'signature' to be a string")
language := ret.GetPropertyAddr("language", false, true)
assert.NotNil(t, language, "Expected a non-nil 'language' property")
assert.True(t, language.Obj().IsString(), "Expected 'language' to be a string")
assert.Equal(t, ".js",
language.Obj().StringValue(), "Expected 'language' to be a string")
environment := ret.GetPropertyAddr("environment", false, true)
assert.NotNil(t, environment, "Expected a non-nil 'environment' property")
envProps := environment.Obj().Properties()
assert.Len(t, envProps.Stable(), 0, "Expected 0 variables in the environment")
}
}