// Copyright 2016-2023, 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. package main import ( "fmt" "path/filepath" "runtime" "runtime/debug" "sync" mapset "github.com/deckarep/golang-set/v2" "github.com/pulumi/pulumi/pkg/v3/display" "github.com/pulumi/pulumi/pkg/v3/resource/deploy" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/util/result" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // L holds the state for the current language test. // // It provides an interface similar to testing.T, // allowing its use with testing libraries like Testify. type L struct { mu sync.RWMutex // guards the fields below // Whether this test has already failed. failed bool // Messages logged to l.Errorf or l.Logf. logs []string // Functions marked helpers with L.Helper(). // These names are from the runtime.Frame.Function field. // They're fully qualified with package names. helpers mapset.Set[string] } // Helper marks the calling function as a test helper function. // When printing file and line information, that function will be skipped. func (l *L) Helper() { pc, _, _, ok := runtime.Caller(1) // skip this function if !ok { return // unlikely but not worth panicking over } frame, _ := runtime.CallersFrames([]uintptr{pc}).Next() if frame.Function == "" { return } l.mu.Lock() defer l.mu.Unlock() if l.helpers == nil { l.helpers = mapset.NewSet[string]() } l.helpers.Add(frame.Function) } // FailNow marks this test as having failed and halts execution. func (l *L) FailNow() { l.Fail() runtime.Goexit() } // Fail marks this test as failed but keeps executing. func (l *L) Fail() { l.mu.Lock() defer l.mu.Unlock() l.failed = true } // Failed returns whether this test has failed. func (l *L) Failed() bool { l.mu.RLock() defer l.mu.RUnlock() return l.failed } // Errorf records the given error message and marks this test as failed. func (l *L) Errorf(format string, args ...interface{}) { l.log(1, fmt.Sprintf(format, args...)) l.Fail() } // Logf records the given message in the L's logs. func (l *L) Logf(format string, args ...interface{}) { l.log(1, fmt.Sprintf(format, args...)) } // log records the given message in the L's logs. // // Skip specifies the number of stack frames to skip // when recording the caller's location. // 0 refers to the immediate caller of log. // // Typically, when used from an exported method on L, // most callers will want to pass skip=1 to skip themselves // and record the location of their caller. func (l *L) log(skip int, msg string) { file, line := "???", 1 if frame, ok := l.callerFrame(skip + 1); ok { file, line = frame.File, frame.Line } msg = fmt.Sprintf("%s:%d: %s", filepath.Base(file), line, msg) l.mu.Lock() l.logs = append(l.logs, msg) l.mu.Unlock() } // Maximal stack depth to search for the caller's frame. const _maxStackDepth = 50 // callerFrame searches the call stack for the first frame // that isn't a helper function. // // skip specifies the initial number of frames to skip // with 0 referring to the immediate caller of callerFrame. func (l *L) callerFrame(skip int) (frame runtime.Frame, ok bool) { var pc [_maxStackDepth]uintptr n := runtime.Callers(skip+2, pc[:]) // skip runtime.Callers and callerFrame if n == 0 { return frame, false } l.mu.RLock() defer l.mu.RUnlock() frames := runtime.CallersFrames(pc[:n]) for { frame, more := frames.Next() if !l.helpers.Contains(frame.Function) { // Not a helper. Use this frame. return frame, true } if !more { break } } return frame, false // no non-helper frames found } // WithL runs the given function with a new L, // blocking until the function returns. // // It returns the information recorded by the L. func WithL(f func(*L)) LResult { // To be able to implement FailNow in the L, // we need to run it in a separate goroutine // so that we can call runtime.Goexit. done := make(chan struct{}) var l L go func() { defer func() { if r := recover(); r != nil { l.failed = true l.logs = append(l.logs, fmt.Sprintf("panic: %v\n\n%s", r, debug.Stack())) } close(done) }() f(&l) }() <-done return LResult{ Failed: l.failed, Messages: l.logs, } } // LResult is the result of running a language test. type LResult struct { // Failed is true if the test failed. Failed bool // Messages contains the messages logged by the test. // // This doesn't necessarily mean that the test failed. // For example, a test may log debugging information // that is only useful when the test fails. Messages []string } // TestingT is a subset of the testing.T interface. // [L] implements this interface. type TestingT interface { Helper() FailNow() Fail() Failed() bool Errorf(string, ...interface{}) Logf(string, ...interface{}) } var ( _ TestingT = (*L)(nil) _ require.TestingT = (TestingT)(nil) // ensure testify compatibility ) func assertStackResource(t TestingT, res result.Result, changes display.ResourceChanges) (ok bool) { t.Helper() ok = true ok = ok && assert.Nil(t, res, "expected no error, got %v", res) ok = ok && assert.NotEmpty(t, changes, "expected at least 1 StepOp") ok = ok && assert.NotZero(t, changes[deploy.OpCreate], "expected at least 1 Create") return ok } func requireStackResource(t TestingT, res result.Result, changes display.ResourceChanges) { t.Helper() if !assertStackResource(t, res, changes) { t.FailNow() } } // assertPropertyMapMember asserts that the given property map has a member with the given key and value. func assertPropertyMapMember( t TestingT, props resource.PropertyMap, key string, want resource.PropertyValue, ) (ok bool) { t.Helper() got, ok := props[resource.PropertyKey(key)] if !assert.True(t, ok, "expected property %q", key) { return false } return assert.Equal(t, want, got, "expected property %q to be %v", key, want) }