pulumi/sdk/go/pulumi/types_test.go
Justin Van Patten 1a4f36e97b
[sdk/go] Add RegisterInputType and register built-in types (#7928)
This change adds a `RegisterInputType` function (similar to the existing `RegisterOutputType`) that is used to register an input interface's type and its associated input type, and adds registrations for the built-in input types.

This will be used when copying inputs to an args struct for multi-lang components. When a field is typed as the input interface (e.g. `StringMapInput`) and doesn't need to be an `Output`, we can use this to lookup the non-Output type that implements the interface (e.g. `StringMap`) so it can be instantiated.

A subsequent change will update the Go SDK codegen to produce input type registrations for a provider's input types.
2021-09-15 21:12:49 -07:00

679 lines
16 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll
package pulumi
import (
"context"
"errors"
"fmt"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func await(out Output) (interface{}, bool, bool, []Resource, error) {
return out.getState().await(context.Background())
}
func assertApplied(t *testing.T, out Output) {
_, known, _, _, err := await(out)
assert.True(t, known)
assert.Nil(t, err)
}
func newIntOutput() IntOutput {
return IntOutput{newOutputState(nil, reflect.TypeOf(42))}
}
func TestBasicOutputs(t *testing.T) {
// Just test basic resolve and reject functionality.
{
out, resolve, _ := NewOutput()
go func() {
resolve(42)
}()
v, known, secret, deps, err := await(out)
assert.Nil(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NotNil(t, v)
assert.Equal(t, 42, v.(int))
}
{
out, _, reject := NewOutput()
go func() {
reject(errors.New("boom"))
}()
v, _, _, _, err := await(out)
assert.NotNil(t, err)
assert.Nil(t, v)
}
}
func TestArrayOutputs(t *testing.T) {
out := ArrayOutput{newOutputState(nil, reflect.TypeOf([]interface{}{}))}
go func() {
out.resolve([]interface{}{nil, 0, "x"}, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(arr []interface{}) (interface{}, error) {
assert.NotNil(t, arr)
if assert.Equal(t, 3, len(arr)) {
assert.Equal(t, nil, arr[0])
assert.Equal(t, 0, arr[1])
assert.Equal(t, "x", arr[2])
}
return nil, nil
}))
}
}
func TestBoolOutputs(t *testing.T) {
out := BoolOutput{newOutputState(nil, reflect.TypeOf(false))}
go func() {
out.resolve(true, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v bool) (interface{}, error) {
assert.True(t, v)
return nil, nil
}))
}
}
func TestMapOutputs(t *testing.T) {
out := MapOutput{newOutputState(nil, reflect.TypeOf(map[string]interface{}{}))}
go func() {
out.resolve(map[string]interface{}{
"x": 1,
"y": false,
"z": "abc",
}, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v map[string]interface{}) (interface{}, error) {
assert.NotNil(t, v)
assert.Equal(t, 1, v["x"])
assert.Equal(t, false, v["y"])
assert.Equal(t, "abc", v["z"])
return nil, nil
}))
}
}
func TestNumberOutputs(t *testing.T) {
out := Float64Output{newOutputState(nil, reflect.TypeOf(float64(0)))}
go func() {
out.resolve(42.345, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v float64) (interface{}, error) {
assert.Equal(t, 42.345, v)
return nil, nil
}))
}
}
func TestStringOutputs(t *testing.T) {
out := StringOutput{newOutputState(nil, reflect.TypeOf(""))}
go func() {
out.resolve("a stringy output", true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v string) (interface{}, error) {
assert.Equal(t, "a stringy output", v)
return nil, nil
}))
}
}
func TestResolveOutputToOutput(t *testing.T) {
// Test that resolving an output to an output yields the value, not the output.
{
out, resolve, _ := NewOutput()
go func() {
other, resolveOther, _ := NewOutput()
resolve(other)
go func() { resolveOther(99) }()
}()
assertApplied(t, out.ApplyT(func(v interface{}) (interface{}, error) {
assert.Equal(t, v, 99)
return nil, nil
}))
}
// Similarly, test that resolving an output to a rejected output yields an error.
{
out, resolve, _ := NewOutput()
go func() {
other, _, rejectOther := NewOutput()
resolve(other)
go func() { rejectOther(errors.New("boom")) }()
}()
v, _, _, _, err := await(out)
assert.NotNil(t, err)
assert.Nil(t, v)
}
}
// Test that ToOutput works with a struct type.
func TestToOutputStruct(t *testing.T) {
out := ToOutput(nestedTypeInputs{Foo: String("bar"), Bar: Int(42)})
_, ok := out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
out = ToOutput(out)
_, ok = out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, deps, err = await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
out = ToOutput(nestedTypeInputs{Foo: ToOutput(String("bar")).(StringInput), Bar: ToOutput(Int(42)).(IntInput)})
_, ok = out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, deps, err = await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
}
type arrayLenInput Array
func (arrayLenInput) ElementType() reflect.Type {
return Array{}.ElementType()
}
func (i arrayLenInput) ToIntOutput() IntOutput {
return i.ToIntOutputWithContext(context.Background())
}
func (i arrayLenInput) ToIntOutputWithContext(ctx context.Context) IntOutput {
return ToOutput(i).ApplyT(func(arr []interface{}) int {
return len(arr)
}).(IntOutput)
}
func (i arrayLenInput) ToIntPtrOutput() IntPtrOutput {
return i.ToIntPtrOutputWithContext(context.Background())
}
func (i arrayLenInput) ToIntPtrOutputWithContext(ctx context.Context) IntPtrOutput {
return ToOutput(i).ApplyT(func(arr []interface{}) *int {
v := len(arr)
return &v
}).(IntPtrOutput)
}
// Test that ToOutput converts inputs appropriately.
func TestToOutputConvert(t *testing.T) {
out := ToOutput(nestedTypeInputs{Foo: ID("bar"), Bar: arrayLenInput{Int(42)}})
_, ok := out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 1}, v)
}
// Test that ToOutput correctly handles nested inputs and outputs when the argument is an input or interface{}.
func TestToOutputAny(t *testing.T) {
type args struct {
S StringInput
I IntInput
A Input
}
out := ToOutput(&args{
S: ID("hello"),
I: Int(42).ToIntOutput(),
A: Map{"world": Bool(true).ToBoolOutput()},
})
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
argsV := v.(*args)
si, ok := argsV.S.(ID)
assert.True(t, ok)
assert.Equal(t, ID("hello"), si)
io, ok := argsV.I.(IntOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), io.state)
assert.Equal(t, 42, io.value)
ai, ok := argsV.A.(Map)
assert.True(t, ok)
bo, ok := ai["world"].(BoolOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), bo.getState().state)
assert.Equal(t, true, bo.value)
}
func TestToOutputAnyDeps(t *testing.T) {
type args struct {
S StringInput
I IntInput
A Input
R Resource
}
stringDep1, stringDep2 := &ResourceState{}, &ResourceState{}
stringOut := StringOutput{newOutputState(nil, reflect.TypeOf(""), stringDep1)}
go func() {
stringOut.resolve("a stringy output", true, false, []Resource{stringDep2})
}()
intDep1, intDep2 := &ResourceState{}, &ResourceState{}
intOut := IntOutput{newOutputState(nil, reflect.TypeOf(0), intDep1)}
go func() {
intOut.resolve(42, true, false, []Resource{intDep2})
}()
boolDep1, boolDep2 := &ResourceState{}, &ResourceState{}
boolOut := BoolOutput{newOutputState(nil, reflect.TypeOf(true), boolDep1)}
go func() {
boolOut.resolve(true, true, false, []Resource{boolDep2})
}()
res := &ResourceState{}
out := ToOutput(&args{
S: stringOut,
I: intOut,
A: Map{"world": boolOut},
R: res,
})
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.ElementsMatch(t, []Resource{stringDep1, stringDep2, intDep1, intDep2, boolDep1, boolDep2, res}, deps)
assert.NoError(t, err)
argsV := v.(*args)
so, ok := argsV.S.(StringOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), so.state)
assert.Equal(t, "a stringy output", so.value)
assert.ElementsMatch(t, []Resource{stringDep1, stringDep2}, so.deps)
io, ok := argsV.I.(IntOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), io.state)
assert.Equal(t, 42, io.value)
assert.ElementsMatch(t, []Resource{intDep1, intDep2}, io.deps)
ai, ok := argsV.A.(Map)
assert.True(t, ok)
bo, ok := ai["world"].(BoolOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), bo.getState().state)
assert.Equal(t, true, bo.value)
assert.ElementsMatch(t, []Resource{boolDep1, boolDep2}, bo.deps)
}
type args struct {
S string
I int
A interface{}
}
type argsInputs struct {
S StringInput
I IntInput
A Input
}
func (*argsInputs) ElementType() reflect.Type {
return reflect.TypeOf((*args)(nil))
}
// Test that ToOutput correctly handles nested inputs when the argument is an input with no corresponding output type.
func TestToOutputInputAny(t *testing.T) {
out := ToOutput(&argsInputs{
S: ID("hello"),
I: Int(42),
A: Map{"world": Bool(true).ToBoolOutput()},
})
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, &args{
S: "hello",
I: 42,
A: map[string]interface{}{"world": true},
}, v)
}
// Test that Unsecret will return an Output that has an unwrapped secret
func TestUnsecret(t *testing.T) {
s := ToSecret(String("foo"))
// assert that secret is immediately secret
assert.True(t, IsSecret(s))
unS := Unsecret(s)
// assert that we do not have a secret
assert.False(t, IsSecret(unS))
errChan := make(chan error)
resultChan := make(chan string)
secretChan := make(chan bool)
unS.ApplyT(func(v interface{}) (string, error) {
// assert secretness after the output resolves
secretChan <- IsSecret(unS)
val := v.(string)
if val == "foo" {
// validate the value
resultChan <- val
} else {
errChan <- fmt.Errorf("Invalid result: %v", val)
}
return val, nil
})
for i := 0; i < 2; i++ {
select {
case err := <-errChan:
assert.Nil(t, err)
break
case r := <-resultChan:
assert.Equal(t, "foo", r)
break
case isSecret := <-secretChan:
assert.False(t, isSecret)
break
}
}
}
// Test that SecretT sets appropriate internal state and that IsSecret appropriately reads it.
func TestSecrets(t *testing.T) {
s := ToSecret(String("foo"))
// assert that secret is immediately secret
assert.True(t, IsSecret(s))
errChan := make(chan error)
resultChan := make(chan string)
secretChan := make(chan bool)
s.ApplyT(func(v interface{}) (string, error) {
// assert secretness after the output resolves
secretChan <- IsSecret(s)
val := v.(string)
if val == "foo" {
// validate the value
resultChan <- val
} else {
errChan <- fmt.Errorf("Invalid result: %v", val)
}
return val, nil
})
for i := 0; i < 2; i++ {
select {
case err := <-errChan:
assert.Nil(t, err)
break
case r := <-resultChan:
assert.Equal(t, "foo", r)
break
case isSecret := <-secretChan:
assert.True(t, isSecret)
break
}
}
}
// Test that secretness is properly bubbled up with all/apply.
func TestSecretApply(t *testing.T) {
s1 := ToSecret(String("foo"))
// assert that secret is immediately secret
assert.True(t, IsSecret(s1))
s2 := StringInput(String("bar"))
errChan := make(chan error)
resultChan := make(chan string)
secretChan := make(chan bool)
s := All(s1, s2).ApplyT(func(v interface{}) (string, error) {
val := v.([]interface{})
return val[0].(string) + val[1].(string), nil
})
s.ApplyT(func(v interface{}) (string, error) {
// assert secretness after the output resolves
secretChan <- IsSecret(s)
val := v.(string)
if val == "foobar" {
// validate the value
resultChan <- val
} else {
errChan <- fmt.Errorf("Invalid result: %v", val)
}
return val, nil
})
for i := 0; i < 2; i++ {
select {
case err := <-errChan:
assert.Nil(t, err)
break
case r := <-resultChan:
assert.Equal(t, "foobar", r)
break
case isSecret := <-secretChan:
assert.True(t, isSecret)
break
}
}
}
func TestNil(t *testing.T) {
ao := Any(nil)
v, known, secret, deps, err := await(ao)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
o := ToOutput(nil)
v, known, secret, deps, err = await(o)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
o = ToOutput(ao)
v, known, secret, deps, err = await(o)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
ao = ToOutput("").ApplyT(func(v string) interface{} {
return nil
}).(AnyOutput)
v, known, secret, deps, err = await(ao)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
bo := ao.ApplyT(func(x interface{}) bool {
return x == nil
})
v, known, secret, deps, err = await(bo)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, true, v)
}
// Test that dependencies flow through all/apply.
func TestDeps(t *testing.T) {
stringDep1, stringDep2 := &ResourceState{}, &ResourceState{}
stringOut := StringOutput{newOutputState(nil, reflect.TypeOf(""), stringDep1)}
assert.ElementsMatch(t, []Resource{stringDep1}, stringOut.deps)
go func() {
stringOut.resolve("hello", true, false, []Resource{stringDep2})
}()
boolDep1, boolDep2 := &ResourceState{}, &ResourceState{}
boolOut := BoolOutput{newOutputState(nil, reflect.TypeOf(true), boolDep1)}
assert.ElementsMatch(t, []Resource{boolDep1}, boolOut.deps)
go func() {
boolOut.resolve(true, true, false, []Resource{boolDep2})
}()
a := All(stringOut, boolOut).ApplyT(func(args []interface{}) (string, error) {
s := args[0].(string)
b := args[1].(bool)
return fmt.Sprintf("%s: %v", s, b), nil
})
v, known, secret, deps, err := await(a)
assert.Equal(t, "hello: true", v)
assert.True(t, known)
assert.False(t, secret)
assert.ElementsMatch(t, []Resource{stringDep1, stringDep2, boolDep1, boolDep2}, deps)
assert.NoError(t, err)
}
func testMixedWaitGroups(t *testing.T, combine func(o1, o2 Output) Output) {
var wg1, wg2 workGroup
o1 := newOutput(&wg1, anyOutputType)
o2 := newOutput(&wg2, anyOutputType)
gate := make(chan chan bool)
combine(o1, o2).ApplyT(func(_ interface{}) interface{} {
<-gate <- true
return 0
})
wg1Done, wg2Done := false, false
go func() {
wg1.Wait()
wg1Done = true
wg2.Wait()
wg2Done = true
}()
o1.getState().resolve(0, true, true, nil)
o2.getState().resolve(0, true, true, nil)
c := make(chan bool)
gate <- c
assert.False(t, wg1Done)
assert.False(t, wg2Done)
<-c
wg1.Wait()
wg2.Wait()
}
func TestMixedWaitGroupsAll(t *testing.T) {
testMixedWaitGroups(t, func(o1, o2 Output) Output {
return All(o1, o2)
})
}
func TestMixedWaitGroupsAny(t *testing.T) {
testMixedWaitGroups(t, func(o1, o2 Output) Output {
return Any(struct{ O1, O2 Output }{o1, o2})
})
}
func TestMixedWaitGroupsApply(t *testing.T) {
testMixedWaitGroups(t, func(o1, o2 Output) Output {
return o1.ApplyT(func(_ interface{}) interface{} {
return o2
})
})
}
type Foo interface {
}
type FooInput interface {
Input
ToFooOutput() Output
}
type FooArgs struct {
}
func (FooArgs) ElementType() reflect.Type {
return nil
}
func TestRegisterInputType(t *testing.T) {
assert.PanicsWithError(t, "expected string to be an interface", func() {
RegisterInputType(reflect.TypeOf(""), FooArgs{})
})
assert.PanicsWithError(t, "expected pulumi.Foo to implement pulumi.Input", func() {
RegisterInputType(reflect.TypeOf((*Foo)(nil)).Elem(), FooArgs{})
})
assert.PanicsWithError(t, "expected pulumi.FooArgs to implement interface pulumi.FooInput", func() {
RegisterInputType(reflect.TypeOf((*FooInput)(nil)).Elem(), FooArgs{})
})
}