blob: 17caee8dc9582271a6b1e02b4db5527ad8973219 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package promising_test
import (
"context"
"errors"
"testing"
"github.com/hashicorp/terraform/internal/promising"
)
func TestMainTaskNoOp(t *testing.T) {
wantVal := "hello"
wantErr := errors.New("hello")
ctx := context.Background()
gotVal, gotErr := promising.MainTask(ctx, func(ctx context.Context) (string, error) {
return wantVal, wantErr
})
if gotVal != wantVal {
t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal)
}
if gotErr != wantErr {
t.Errorf("wrong error\ngot: %q\nwant: %q", gotErr, wantErr)
}
}
func TestPromiseResolveSimple(t *testing.T) {
wantVal := "hello"
ctx := context.Background()
gotVal, err := promising.MainTask(ctx, func(ctx context.Context) (string, error) {
resolver, get := promising.NewPromise[string](ctx, "test")
promising.AsyncTask(
ctx, resolver,
func(ctx context.Context, resolver promising.PromiseResolver[string]) {
resolver.Resolve(ctx, wantVal, nil)
},
)
return get(ctx)
})
if gotVal != wantVal {
t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal)
}
if err != nil {
t.Errorf("unexpected error: %s", err)
}
}
func TestPromiseUnresolvedMainWithoutGet(t *testing.T) {
ctx := context.Background()
var promiseID promising.PromiseID
gotVal, err := promising.MainTask(ctx, func(ctx context.Context) (string, error) {
resolver, _ := promising.NewPromise[string](ctx, "test")
promiseID = resolver.PromiseID()
// Call to PromiseResolver.Resolve intentionally omitted to cause error
// Also not calling the getter to prevent this from being classified as
// a self-dependency.
return "", nil
})
if wantVal := ""; gotVal != wantVal {
// When unresolved the return value should be the zero value of the type.
t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal)
}
if promiseIDs, ok := err.(promising.ErrUnresolved); !ok {
t.Errorf("wrong error\ngot: %s\nwant: an ErrUnresolved value", err)
} else if got, want := len(promiseIDs), 1; got != want {
t.Errorf("wrong number of unresolved promises %d; want %d", got, want)
} else if promiseIDs[0] != promiseID {
t.Error("errored promise ID does not match the one returned during the task")
}
}
func TestPromiseUnresolvedMainWithGet(t *testing.T) {
ctx := context.Background()
var promiseID promising.PromiseID
gotVal, gotErr := promising.MainTask(ctx, func(ctx context.Context) (string, error) {
resolver, get := promising.NewPromise[string](ctx, "test")
promiseID = resolver.PromiseID()
// Call to PromiseResolver.Resolve intentionally omitted to cause error
return get(ctx)
})
if wantVal := ""; gotVal != wantVal {
// When unresolved the return value should be the zero value of the type.
t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal)
}
// Since the main task was both the one responsible for the promise and
// the one trying to read it, this is classified as a self-dependency
// rather than an "unresolved".
if err, ok := gotErr.(promising.ErrSelfDependent); ok {
if got, want := len(err), 1; got != want {
t.Fatalf("wrong number of promise IDs in error %d; want %d", got, want)
}
if got, want := err[0], promiseID; got != want {
t.Errorf("wrong promise ID in error\ngot: %#v\nwant: %#v", got, want)
}
} else {
t.Errorf("wrong error\ngot: %s\nwant: an ErrSelfDependent value", gotErr)
}
}
func TestPromiseUnresolvedAsync(t *testing.T) {
ctx := context.Background()
var promiseID promising.PromiseID
gotVal, err := promising.MainTask(ctx, func(ctx context.Context) (string, error) {
resolver, get := promising.NewPromise[string](ctx, "test")
promiseID = resolver.PromiseID()
promising.AsyncTask(
ctx, resolver,
func(ctx context.Context, resolver promising.PromiseResolver[string]) {
// Call to resolver.Resolve intentionally omitted to cause error
},
)
return get(ctx)
})
if wantVal := ""; gotVal != wantVal {
// When unresolved the return value should be the zero value of the type.
t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal)
}
if promiseIDs, ok := err.(promising.ErrUnresolved); !ok {
t.Errorf("wrong error\ngot: %s\nwant: an ErrUnresolved value", err)
} else if got, want := len(promiseIDs), 1; got != want {
t.Errorf("wrong number of unresolved promises %d; want %d", got, want)
} else if promiseIDs[0] != promiseID {
t.Error("errored promise ID does not match the one returned during the task")
}
}
func TestPromiseSelfDependentSibling(t *testing.T) {
ctx := context.Background()
var err1, err2 error
promising.MainTask(ctx, func(ctx context.Context) (string, error) {
resolver1, get1 := promising.NewPromise[string](ctx, "test")
resolver2, get2 := promising.NewPromise[string](ctx, "test")
// The following is an intentional self-dependency, though its
// unpredictable which of the two tasks will actually detect the error,
// since it'll be whichever one reaches its getter second.
promising.AsyncTask(
ctx, resolver1,
func(ctx context.Context, resolver1 promising.PromiseResolver[string]) {
v, err := get2(ctx)
resolver1.Resolve(ctx, v, err)
},
)
promising.AsyncTask(
ctx, resolver2,
func(ctx context.Context, resolver1 promising.PromiseResolver[string]) {
v, err := get1(ctx)
resolver2.Resolve(ctx, v, err)
},
)
_, err1 = get1(ctx)
_, err2 = get2(ctx)
return "", nil
})
switch {
case err1 == nil && err2 == nil:
t.Fatalf("both promises succeeded; expected both to fail")
case err1 == nil:
t.Fatalf("first promise succeeded; expected both to fail")
case err2 == nil:
t.Fatalf("second promise succeeded; expected both to fail")
}
if err, ok := err1.(promising.ErrSelfDependent); ok {
if got, want := len(err), 2; got != want {
t.Fatalf("wrong number of promise IDs in err1 %d; want %d", got, want)
}
} else {
t.Errorf("wrong err1\ngot: %s\nwant: an ErrSelfDependent value", err1)
}
if err, ok := err2.(promising.ErrSelfDependent); ok {
if got, want := len(err), 2; got != want {
t.Fatalf("wrong number of promise IDs in err2 %d; want %d", got, want)
}
} else {
t.Errorf("wrong err2\ngot: %s\nwant: an ErrSelfDependent value", err2)
}
}
func TestPromiseSelfDependentNested(t *testing.T) {
ctx := context.Background()
var err1, err2 error
promising.MainTask(ctx, func(ctx context.Context) (string, error) {
resolver1, get1 := promising.NewPromise[string](ctx, "test")
resolver2, get2 := promising.NewPromise[string](ctx, "test")
pair := promising.PromiseResolverPair[string, string]{A: resolver1, B: resolver2}
// The following is an intentional self-dependency. Both calls should
// fail here, since a self-dependency problem causes all affected
// promises to immediately emit an error.
promising.AsyncTask(
ctx, pair,
func(ctx context.Context, pair promising.PromiseResolverPair[string, string]) {
resolver1 := pair.A
resolver2 := pair.B
promising.AsyncTask(
ctx, resolver2,
func(ctx context.Context, resolver1 promising.PromiseResolver[string]) {
v, err := get1(ctx)
resolver2.Resolve(ctx, v, err)
},
)
v, err := get2(ctx)
resolver1.Resolve(ctx, v, err)
},
)
_, err1 = get1(ctx)
_, err2 = get2(ctx)
return "", nil
})
switch {
case err1 == nil && err2 == nil:
t.Fatalf("both promises succeeded; expected both to fail")
case err1 == nil:
t.Fatalf("first promise succeeded; expected both to fail")
case err2 == nil:
t.Fatalf("second promise succeeded; expected both to fail")
}
if err, ok := err1.(promising.ErrSelfDependent); ok {
if got, want := len(err), 2; got != want {
t.Fatalf("wrong number of promise IDs in err1 %d; want %d", got, want)
}
} else {
t.Errorf("wrong err1\ngot: %s\nwant: an ErrSelfDependent value", err1)
}
if err, ok := err2.(promising.ErrSelfDependent); ok {
if got, want := len(err), 2; got != want {
t.Fatalf("wrong number of promise IDs in err2 %d; want %d", got, want)
}
} else {
t.Errorf("wrong err2\ngot: %s\nwant: an ErrSelfDependent value", err2)
}
}