github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/testing/test/helpers.go (about)

     1  // Copyright 2016-2021, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package test
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"path/filepath"
    25  	"sort"
    26  	"testing"
    27  
    28  	"github.com/stretchr/testify/assert"
    29  	"github.com/stretchr/testify/require"
    30  	"gopkg.in/yaml.v3"
    31  
    32  	"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
    33  	"github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils"
    34  	"github.com/pulumi/pulumi/pkg/v3/testing/integration"
    35  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/executable"
    36  )
    37  
    38  // GenPkgSignature corresponds to the shape of the codegen GeneratePackage functions.
    39  type GenPkgSignature func(string, *schema.Package, map[string][]byte) (map[string][]byte, error)
    40  
    41  // GeneratePackageFilesFromSchema loads a schema and generates files using the provided GeneratePackage function.
    42  func GeneratePackageFilesFromSchema(schemaPath string, genPackageFunc GenPkgSignature) (map[string][]byte, error) {
    43  	// Read in, decode, and import the schema.
    44  	schemaBytes, err := ioutil.ReadFile(schemaPath)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	ext := filepath.Ext(schemaPath)
    50  
    51  	var pkgSpec schema.PackageSpec
    52  	if ext == ".yaml" || ext == ".yml" {
    53  		err = yaml.Unmarshal(schemaBytes, &pkgSpec)
    54  	} else {
    55  		err = json.Unmarshal(schemaBytes, &pkgSpec)
    56  	}
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
    62  	pkg, diags, err := schema.BindSpec(pkgSpec, loader)
    63  	if err != nil {
    64  		return nil, err
    65  	} else if diags.HasErrors() {
    66  		return nil, diags
    67  	}
    68  
    69  	return genPackageFunc("test", pkg, nil)
    70  }
    71  
    72  // LoadFiles loads the provided list of files from a directory.
    73  func LoadFiles(dir, lang string, files []string) (map[string][]byte, error) {
    74  	result := map[string][]byte{}
    75  	for _, file := range files {
    76  		fileBytes, err := ioutil.ReadFile(filepath.Join(dir, lang, file))
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  
    81  		result[file] = fileBytes
    82  	}
    83  
    84  	return result, nil
    85  }
    86  
    87  func PathExists(path string) (bool, error) {
    88  	_, err := os.Stat(path)
    89  
    90  	if os.IsNotExist(err) {
    91  		return false, nil
    92  	}
    93  
    94  	if err == nil {
    95  		return true, nil
    96  	}
    97  
    98  	return false, err
    99  }
   100  
   101  // `LoadBaseline` loads the contents of the given baseline directory,
   102  // by inspecting its `codegen-manifest.json`.
   103  func LoadBaseline(dir, lang string) (map[string][]byte, error) {
   104  	cm := &codegenManifest{}
   105  	err := cm.load(filepath.Join(dir, lang))
   106  	if err != nil {
   107  		return nil, fmt.Errorf("Failed to load codegen-manifest.json: %w", err)
   108  	}
   109  
   110  	files := make(map[string][]byte)
   111  
   112  	for _, f := range cm.EmittedFiles {
   113  		bytes, err := ioutil.ReadFile(filepath.Join(dir, lang, f))
   114  		if err != nil {
   115  			return nil, fmt.Errorf("Failed to load file %s referenced in codegen-manifest.json: %w", f, err)
   116  		}
   117  		files[f] = bytes
   118  	}
   119  
   120  	return files, nil
   121  }
   122  
   123  type codegenManifest struct {
   124  	EmittedFiles []string `json:"emittedFiles"`
   125  }
   126  
   127  func (cm *codegenManifest) load(dir string) error {
   128  	bytes, err := ioutil.ReadFile(filepath.Join(dir, "codegen-manifest.json"))
   129  	if err != nil {
   130  		return err
   131  	}
   132  	return json.Unmarshal(bytes, cm)
   133  }
   134  
   135  func (cm *codegenManifest) save(dir string) error {
   136  	sort.Strings(cm.EmittedFiles)
   137  	buf := &bytes.Buffer{}
   138  	enc := json.NewEncoder(buf)
   139  	enc.SetEscapeHTML(false)
   140  	enc.SetIndent("", "  ")
   141  	err := enc.Encode(cm)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	data := buf.Bytes()
   146  	return ioutil.WriteFile(filepath.Join(dir, "codegen-manifest.json"), data, 0600)
   147  }
   148  
   149  // ValidateFileEquality compares maps of files for equality.
   150  func ValidateFileEquality(t *testing.T, actual, expected map[string][]byte) bool {
   151  	ok := true
   152  	for name, file := range expected {
   153  		_, inActual := actual[name]
   154  		if inActual {
   155  			if !assert.Equal(t, string(file), string(actual[name]), name) {
   156  				t.Logf("%s did not agree", name)
   157  				ok = false
   158  			}
   159  		} else {
   160  			t.Logf("File %s was expected but is missing from the actual fileset", name)
   161  			ok = false
   162  		}
   163  	}
   164  	for name := range actual {
   165  		if _, inExpected := expected[name]; !inExpected {
   166  			t.Logf("File %s from the actual fileset was not expected", name)
   167  			ok = false
   168  		}
   169  	}
   170  	return ok
   171  }
   172  
   173  // If PULUMI_ACCEPT is set, writes out actual output to the expected
   174  // file set, so we can continue enjoying golden tests without manually
   175  // modifying the expected output.
   176  func RewriteFilesWhenPulumiAccept(t *testing.T, dir, lang string, actual map[string][]byte) bool {
   177  	if os.Getenv("PULUMI_ACCEPT") == "" {
   178  		return false
   179  	}
   180  
   181  	cm := &codegenManifest{}
   182  
   183  	baseline := filepath.Join(dir, lang)
   184  
   185  	// Remove the baseline directory's current contents.
   186  	_, err := os.ReadDir(baseline)
   187  	switch {
   188  	case err == nil:
   189  		err = os.RemoveAll(baseline)
   190  		require.NoError(t, err)
   191  	case os.IsNotExist(err):
   192  		// OK
   193  	default:
   194  		require.NoError(t, err)
   195  	}
   196  
   197  	for file, bytes := range actual {
   198  		relPath := filepath.FromSlash(file)
   199  		path := filepath.Join(dir, lang, relPath)
   200  		cm.EmittedFiles = append(cm.EmittedFiles, relPath)
   201  		err := writeFileEnsuringDir(path, bytes)
   202  		require.NoError(t, err)
   203  	}
   204  
   205  	err = cm.save(filepath.Join(dir, lang))
   206  	require.NoError(t, err)
   207  
   208  	return true
   209  }
   210  
   211  // Useful for populating code-generated destination
   212  // `codeDir=$dir/$lang` with extra manually written files such as the
   213  // unit test files. These files are copied from `$dir/$lang-extras`
   214  // folder if present.
   215  func CopyExtraFiles(t *testing.T, dir, lang string) {
   216  	codeDir := filepath.Join(dir, lang)
   217  	extrasDir := filepath.Join(dir, fmt.Sprintf("%s-extras", lang))
   218  	gotExtras, err := PathExists(extrasDir)
   219  
   220  	if !gotExtras {
   221  		return
   222  	}
   223  
   224  	if err != nil {
   225  		require.NoError(t, err)
   226  		return
   227  	}
   228  
   229  	err = filepath.Walk(extrasDir, func(path string, info os.FileInfo, err error) error {
   230  		if err != nil {
   231  			return err
   232  		}
   233  		if info.IsDir() {
   234  			return nil
   235  		}
   236  		relPath, err := filepath.Rel(extrasDir, path)
   237  		if err != nil {
   238  			return err
   239  		}
   240  		destPath := filepath.Join(codeDir, relPath)
   241  
   242  		bytes, err := ioutil.ReadFile(path)
   243  		if err != nil {
   244  			return err
   245  		}
   246  		err = writeFileEnsuringDir(destPath, bytes)
   247  		if err != nil {
   248  			return err
   249  		}
   250  		t.Logf("Copied %s to %s", path, destPath)
   251  		return nil
   252  	})
   253  
   254  	require.NoError(t, err)
   255  }
   256  
   257  func writeFileEnsuringDir(path string, bytes []byte) error {
   258  	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil && !os.IsExist(err) {
   259  		return err
   260  	}
   261  
   262  	return ioutil.WriteFile(path, bytes, 0600)
   263  }
   264  
   265  // CheckAllFilesGenerated ensures that the set of expected and actual files generated
   266  // are exactly equivalent.
   267  func CheckAllFilesGenerated(t *testing.T, actual, expected map[string][]byte) {
   268  	seen := map[string]bool{}
   269  	for x := range expected {
   270  		seen[x] = true
   271  	}
   272  	for a := range actual {
   273  		assert.Contains(t, seen, a, "Unexpected file generated: %s", a)
   274  		if seen[a] {
   275  			delete(seen, a)
   276  		}
   277  	}
   278  
   279  	for s := range seen {
   280  		assert.Fail(t, "No content generated for expected file %s", s)
   281  	}
   282  }
   283  
   284  // Validates a transformer on a single file.
   285  func ValidateFileTransformer(
   286  	t *testing.T,
   287  	inputFile string,
   288  	expectedOutputFile string,
   289  	transformer func(reader io.Reader, writer io.Writer) error) {
   290  
   291  	reader, err := os.Open(inputFile)
   292  	if err != nil {
   293  		t.Error(err)
   294  		return
   295  	}
   296  
   297  	var buf bytes.Buffer
   298  
   299  	err = transformer(reader, &buf)
   300  	if err != nil {
   301  		t.Error(err)
   302  		return
   303  	}
   304  
   305  	actualBytes := buf.Bytes()
   306  
   307  	if os.Getenv("PULUMI_ACCEPT") != "" {
   308  		err := ioutil.WriteFile(expectedOutputFile, actualBytes, 0600)
   309  		if err != nil {
   310  			t.Error(err)
   311  			return
   312  		}
   313  	}
   314  
   315  	actual := map[string][]byte{expectedOutputFile: actualBytes}
   316  
   317  	expectedBytes, err := ioutil.ReadFile(expectedOutputFile)
   318  	if err != nil {
   319  		t.Error(err)
   320  		return
   321  	}
   322  
   323  	expected := map[string][]byte{expectedOutputFile: expectedBytes}
   324  
   325  	ValidateFileEquality(t, actual, expected)
   326  }
   327  
   328  func RunCommand(t *testing.T, name string, cwd string, exec string, args ...string) {
   329  	RunCommandWithOptions(t, &integration.ProgramTestOptions{}, name, cwd, exec, args...)
   330  }
   331  
   332  func RunCommandWithOptions(
   333  	t *testing.T,
   334  	opts *integration.ProgramTestOptions,
   335  	name string, cwd string, exec string, args ...string) {
   336  
   337  	exec, err := executable.FindExecutable(exec)
   338  	if err != nil {
   339  		t.Error(err)
   340  		t.FailNow()
   341  	}
   342  	wd, err := filepath.Abs(cwd)
   343  	require.NoError(t, err)
   344  	var stdout, stderr bytes.Buffer
   345  	opts.Stdout = &stdout
   346  	opts.Stderr = &stderr
   347  	opts.Verbose = true
   348  	err = integration.RunCommand(t,
   349  		name,
   350  		append([]string{exec}, args...),
   351  		wd,
   352  		opts)
   353  	if !assert.NoError(t, err) {
   354  		stdout := stdout.String()
   355  		stderr := stderr.String()
   356  		if len(stdout) > 0 {
   357  			t.Logf("stdout: %s", stdout)
   358  		}
   359  		if len(stderr) > 0 {
   360  			t.Logf("stderr: %s", stderr)
   361  		}
   362  		t.FailNow()
   363  	}
   364  }
   365  
   366  type SchemaVersion = string
   367  
   368  // Schemas are downloaded in the makefile, and the versions specified here
   369  // should be in sync with the makefile.
   370  const (
   371  	AwsSchema         SchemaVersion = "4.26.0"
   372  	AzureNativeSchema SchemaVersion = "1.29.0"
   373  	AzureSchema       SchemaVersion = "4.18.0"
   374  	KubernetesSchema  SchemaVersion = "3.7.2"
   375  	RandomSchema      SchemaVersion = "4.2.0"
   376  	EksSchema         SchemaVersion = "0.37.1"
   377  )