Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add matrix testing #13705

Merged
merged 40 commits into from Sep 13, 2023
Merged

Add matrix testing #13705

merged 40 commits into from Sep 13, 2023

Conversation

Frassle
Copy link
Member

@Frassle Frassle commented Aug 10, 2023

Description

Adds the first pass of matrix testing.

Matrix testing allows us to define tests once in pulumi/pulumi via PCL and then run those tests against each language plugin to verify code generation and runtime correctness.

Rather than packing matrix tests and all the associated data and machinery into the CLI itself we define a new Go package at cmd/pulumi-test-lanaguage. This depends on pkg and runs the deployment engine in a unique way for matrix tests but it is running the proper deployment engine with a proper backend (always filestate, using $TEMP).

Currently only NodeJS is hooked up to run these tests, and all the code for that currently lives in sdk/nodejs/cmd/pulumi-language-nodejs/language_test.go. I expect we'll move that helper code to sdk/go/common and use it in each language plugin to run the tests in the same way.

This first pass includes 3 simple tests:

  • l1-empty that runs an empty PCL file and checks just a stack is created
  • l1-output-bool that runs a PCL program that returns two stack outputs of true and `false
  • l2-resource-simple that runs a PCL program creating a simple resource with a single bool property

These tests are themselves tested with a mock language runtime. This verifies the behavior of the matrix test framework for both correct and incorrect language hosts (that is some the mock language runtimes purposefully cause errors or compute the wrong result).

There are a number of things missing from from the core framework still, but I feel don't block getting this first pass merged and starting to be used.

  1. The tests can not currently run in parallel. That is calling RunLanguageTest in parallel will break things. This is due to two separate problems. Firstly is that the SDK snapshot's are not safe to write in parallel (when PULUMI_ACCEPT is true), this should be fairly easy to fix by doing a write to dst-{random} and them atomic move to dst. Secondly is that the deployment engine itself has mutable global state, short term we should probably just lock around that part RunLanguageTest, long term it would be good to clean that up.
  2. We need a way to verify "preview" behavior, I think this is probably just a variation of the tests that would call stack.Preview and not pass a snapshot to assert.
  3. stdout, stderr and log messages are returned in bulk at the end of the test. Plus there are a couple of calls to the language runtime that don't correctly thread stdout/stderr to use and so default to the process os.Stdout/Stderr. stdout/stderr streaming shows up in a load of other places as well so I'm thinking of a clean way to handle all of them together. Log message streaming we can probably do by just turning RunLanguageTest to a streaming grpc call.

Checklist

  • I have run make tidy to update any new dependencies
  • I have run make lint to verify my code passes the lint check
    • I have formatted my code using gofumpt
  • I have added tests that prove my fix is effective or that my feature works
  • I have run make changelog and committed the changelog/pending/<file> documenting my change
  • Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version

@pulumi-bot
Copy link
Contributor

pulumi-bot commented Aug 10, 2023

Changelog

[uncommitted] (2023-09-13)

Features

  • [engine] pulumi-test-language can now be used to test language runtimes against a standard suite of tests.
    #13705

Currently, assertions are hand written as functions
that return `(ok bool, msg string)`.
Long-term, this will hurt readability
and has a higher risk of introducing bugs in testing
because conditions in assertions will be inverted
from what most people are used to
(checking the negative rather than the positive case).

This change introduces the ability to write these assertions
using a testing.T-compatible library like testify.

To do this, we introduce a new `*L` type analogous to `*T`
with an API similar to `testing.T`.
It implements more than sufficient methods to satisfy
Testify's TestingT interface, allowing its use with testify.

    // The following is all that is necessary for Testify.
    type L struct{ /* ... */ }
    func (*L) Errorf(string, ...any)
    func (*L) FailNow()

A function `WithL` is provided that constructs an `*L`,
runs the provided function, and returns the result.

    func WithL(func(*L)) LResult

`L` should not be instantiated directly, without this function,
as this function handles the goroutine management necessary
to make `FailNow()` halt execution for `require`-based assertions.

Finally, this alters the contract for these tests, slightly.
Previously, the result of a test was represented roughly by the type:

    type Result = enum {
        Success,
        Failure: { message: string }
    }

That is, either a test is successful, or it failed and has a message.

With this change, the result is represented with the type,

    type Result = {
        messages: []string,
        status: enum { Success, Failure }
    }

That is, messages may be included regardless of success or failure.
This brings it more in line with how `go test` works:
`t.Log` and `t.Error` both log messages,
one just marks the test as failed.

As of right now, messages are sent back to the test runner
at the end of the language test.
In a future change, we should explore streaming these messages back
into the `testing.T`.
Adds an `L.Helper()` method that marks the caller
as a helper function.
Functions that call this will be skipped when gathering
position information about where a log message comes from.

This is especially desirable as all of testify's assertion functions
call this method if it exists.
Without this, all testify messages will start at assertions.go
versus the actual test file.
@abhinav
Copy link
Contributor

abhinav commented Aug 21, 2023

Updates:
Per prior discussion, I've pushed a couple commits that add a testing.T-style API, allowing use of testify inside test cases instead of hand-rolled (ok bool, msg string)-based assertions.

@abhinav
Copy link
Contributor

abhinav commented Aug 22, 2023

Status for @Frassle when you're back:

  • Testify/testing.T-style API works great. Helpers can be defined as if they were defined for testing.T. We should use these APIs.
  • The language-specific integrations (the thing that calls RunLanguageTest) need to all be a bit smarter:
    • there should be some helper glue code somewhere for each language's entry point that runs the language test and prints the stdout, stderr, and message to if the test failed—currently each test does this individually.
    • by default, the integration should mark the current test as failed if the RunLanguageTest reports a failure. Allowing a failure should not be the default, and be explicitly requested.
  • In the future, we should consider turning message, stdout, and stderr into a stream of log messages; stream them to the test log as they occur. Will be helpful if a test fails too early.
  • Per Ideation discussion, we should also pull the language test subcommand out of the pulumi CLI into its own independent purpose-built CLI that we build on-demand in each consumer (e.g. go install github.com/pulumi/pulumi/pkg/v3/cmd/pulumi-engine)

@Frassle Frassle marked this pull request as ready for review September 1, 2023 21:55
@Frassle Frassle requested a review from a team September 1, 2023 21:56
Copy link
Contributor

@abhinav abhinav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimmed changes since I poked around at a high level. Seems reasonable, although I haven't evaluated the specifics of the tests implemented. Will defer to @pulumi/platform friends for actual review.

Comment on lines 262 to 264
if !bytes.Equal(actualContents, expectedContents) {
// TODO: Find a way to show better diffs here
validations = append(validations, fmt.Sprintf("expected file %s does not match actual file", relativePath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider go-cmp or pkg/diff. The latter prints something closer to a git diff-style unified diff.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

Comment on lines +169 to +175
for testName := range languageTests {
// Don't return internal tests
if strings.HasPrefix(testName, "internal-") {
continue
}
tests = append(tests, testName)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

languageTests is a map, so the order of items in tests will be non-deterministic. Should this have a sort.Strings at the end?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order shouldn't really matter, but if it did matter we'd want declaration order. Could either change this to a list (but then we get slower lookups) or add a counter to each item in the array and sort by that here.

Copy link
Member

@justinvp justinvp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Can we add links to tracking issues for the TODOs?

@@ -0,0 +1,26 @@
"use strict";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to re-run this to delete this file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, might need to fix the snapshot code to delete everything before writing new as well.

func TestLanguage(t *testing.T) {
t.Parallel()

os.Setenv("PULUMI_ACCEPT", "true")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we always want this set?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this was left over from working on this.

@@ -242,7 +242,7 @@ func (host *nodeLanguageHost) GetRequiredPlugins(ctx context.Context,
// minor version, we will issue a warning to the user.
pulumiPackagePathToVersionMap := make(map[string]semver.Version)
plugins, err := getPluginsFromDir(
req.GetProgram(),
filepath.Join(req.Pwd, req.Program),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this change break anyone? I have a heightened sense for changes here now that this is the primary way plugins will be determined for Node.js projects.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so.
Program is a relative path from the project root to the program, and we normally start language plugins with their working directory set to the project root.
This now just does that explicitly because when we're doing matrix testing the plugins working directory is the test directory.
We have quite a few tests for odd combinations of this in the integration tests and none of those flagged this as an issue.

@Frassle Frassle added this pull request to the merge queue Sep 13, 2023
Merged via the queue into master with commit c27d768 Sep 13, 2023
50 checks passed
@Frassle Frassle deleted the fraser/matrix branch September 13, 2023 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants