From 872c7661e3017c5ca4964895e6dadba191dab824 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 14 Nov 2018 13:27:32 -0800 Subject: [PATCH] Provide a way to override packages during a test run Add a new property to ProgramTestOptions, `Overrides` that allows a test to request a different version of a package is used instead of what would be listed in the package.json file. This will be used by our nightly automation to run everything "at head" --- examples/examples_test.go | 36 +++++++------- pkg/testing/integration/program.go | 76 +++++++++++++++++++++++++++++- pkg/testing/integration/util.go | 20 ++++++++ 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/examples/examples_test.go b/examples/examples_test.go index 73efc8d74..38c213429 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -21,26 +21,23 @@ func TestExamples(t *testing.T) { return } - var minimal integration.ProgramTestOptions - minimal = integration.ProgramTestOptions{ - Dir: path.Join(cwd, "minimal"), - Dependencies: []string{"@pulumi/pulumi"}, - Config: map[string]string{ - "name": "Pulumi", - }, - Secrets: map[string]string{ - "secret": "this is my secret message", - }, - ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { - // Simple runtime validation that just ensures the checkpoint was written and read. - assert.NotNil(t, stackInfo.Deployment) - }, - RunBuild: true, - } - var formattableStdout, formattableStderr bytes.Buffer examples := []integration.ProgramTestOptions{ - minimal, + { + Dir: path.Join(cwd, "minimal"), + Dependencies: []string{"@pulumi/pulumi"}, + Config: map[string]string{ + "name": "Pulumi", + }, + Secrets: map[string]string{ + "secret": "this is my secret message", + }, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + // Simple runtime validation that just ensures the checkpoint was written and read. + assert.NotNil(t, stackInfo.Deployment) + }, + RunBuild: true, + }, { Dir: path.Join(cwd, "dynamic-provider/simple"), Dependencies: []string{"@pulumi/pulumi"}, @@ -82,8 +79,7 @@ func TestExamples(t *testing.T) { Dependencies: []string{"@pulumi/pulumi"}, }, { - Dir: path.Join(cwd, "compat/v0.10.0/minimal"), - Dependencies: []string{"@pulumi/pulumi"}, + Dir: path.Join(cwd, "compat/v0.10.0/minimal"), Config: map[string]string{ "name": "Pulumi", }, diff --git a/pkg/testing/integration/program.go b/pkg/testing/integration/program.go index ca918fd62..6fec0eb19 100644 --- a/pkg/testing/integration/program.go +++ b/pkg/testing/integration/program.go @@ -115,9 +115,12 @@ type ProgramTestOptions struct { Dir string // Array of NPM packages which must be `yarn linked` (e.g. {"pulumi", "@pulumi/aws"}) Dependencies []string - // Map of config keys and values to set (e.g. {"aws:config:region": "us-east-2"}) + // Map of package names to versions. The test will use the specified versions of these packages instead of what + // is declared in `package.json`. + Overrides map[string]string + // Map of config keys and values to set (e.g. {"aws:region": "us-east-2"}) Config map[string]string - // Map of secure config keys and values to set on the stack (e.g. {"aws:config:region": "us-east-2"}) + // Map of secure config keys and values to set on the stack (e.g. {"aws:region": "us-east-2"}) Secrets map[string]string // EditDirs is an optional list of edits to apply to the example, as subsequent deployments. EditDirs []EditDir @@ -270,6 +273,9 @@ func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOpt if overrides.Dependencies != nil { opts.Dependencies = overrides.Dependencies } + if overrides.Overrides != nil { + opts.Overrides = overrides.Overrides + } for k, v := range overrides.Config { if opts.Config == nil { opts.Config = make(map[string]string) @@ -1130,10 +1136,46 @@ func (pt *programTester) prepareNodeJSProject(projinfo *engine.Projinfo) error { return err } + // If the test requested some packages to be overridden, we do two things. First, if the package is listed as a + // direct dependency of the project, we change the version constraint in the package.json. For transitive + // dependeices, we use yarn's "resolutions" feature to force them to a specific version. + if len(pt.opts.Overrides) > 0 { + packageJSON, err := readPackageJSON(cwd) + if err != nil { + return err + } + + overrides := make(map[string]interface{}) + + for packageName, packageVersion := range pt.opts.Overrides { + for _, section := range []string{"dependencies", "devDependencies"} { + if _, has := packageJSON[section]; has { + entry := packageJSON[section].(map[string]interface{}) + + if _, has := entry[packageName]; has { + entry[packageName] = packageVersion + } + + } + } + + fprintf(pt.opts.Stdout, "adding resolution for %s to version %s\n", packageName, packageVersion) + overrides["**/"+packageName] = packageVersion + } + + // Wack any existing overrides section with our newly computed one. + packageJSON["overrides"] = overrides + + if err := writePackageJSON(cwd, packageJSON); err != nil { + return err + } + } + // Now ensure dependencies are present. if err = pt.runYarnCommand("yarn-install", []string{"install", "--verbose"}, cwd); err != nil { return err } + for _, dependency := range pt.opts.Dependencies { if err = pt.runYarnCommand("yarn-link", []string{"link", dependency}, cwd); err != nil { return err @@ -1151,6 +1193,36 @@ func (pt *programTester) prepareNodeJSProject(projinfo *engine.Projinfo) error { } +// readPackageJSON unmarshals the package.json file located in pathToPackage. +func readPackageJSON(pathToPackage string) (map[string]interface{}, error) { + f, err := os.Open(filepath.Join(pathToPackage, "package.json")) + if err != nil { + return nil, errors.Wrap(err, "opening package.json") + } + defer contract.IgnoreClose(f) + + var ret map[string]interface{} + if err := json.NewDecoder(f).Decode(&ret); err != nil { + return nil, errors.Wrap(err, "decoding package.json") + } + + return ret, nil +} + +func writePackageJSON(pathToPackage string, metadata map[string]interface{}) error { + // os.Create truncates the already existing file. + f, err := os.Create(filepath.Join(pathToPackage, "package.json")) + if err != nil { + return errors.Wrap(err, "opening package.json") + } + defer contract.IgnoreClose(f) + + encoder := json.NewEncoder(f) + encoder.SetIndent("", " ") + + return errors.Wrap(encoder.Encode(metadata), "writing package.json") +} + // preparePythonProject runs setup necessary to get a Python project ready for `pulumi` commands. func (pt *programTester) preparePythonProject(projinfo *engine.Projinfo) error { cwd, _, err := projinfo.GetPwdMain() diff --git a/pkg/testing/integration/util.go b/pkg/testing/integration/util.go index ed0ac09f5..0bf02b969 100644 --- a/pkg/testing/integration/util.go +++ b/pkg/testing/integration/util.go @@ -30,6 +30,26 @@ import ( "github.com/pulumi/pulumi/pkg/util/contract" ) +// DecodeMapString takes a string of the form key1=value1:key2=value2 and returns a go map. +func DecodeMapString(val string) (map[string]string, error) { + newMap := make(map[string]string) + + if val != "" { + for _, overrideClause := range strings.Split(val, ":") { + data := strings.Split(overrideClause, "=") + if len(data) != 2 { + return nil, errors.Errorf( + "could not decode %s as an override, should be of the form =", overrideClause) + } + packageName := data[0] + packageVersion := data[1] + newMap[packageName] = packageVersion + } + } + + return newMap, nil +} + // ReplaceInFile does a find and replace for a given string within a file. func ReplaceInFile(old, new, path string) error { rawContents, err := ioutil.ReadFile(path)