Skip to content

Add repro test for MaxItems=1 update panic - #3390

Draft
corymhall wants to merge 1 commit into
mainfrom
gcp-3666
Draft

Add repro test for MaxItems=1 update panic#3390
corymhall wants to merge 1 commit into
mainfrom
gcp-3666

Conversation

@corymhall

Copy link
Copy Markdown
Member

Summary

  • add focused repro coverage for the pulumi/pulumi-gcp#3666 MaxItems=1 update failure
  • add a targeted SDKv2 update-path test that reproduces the panic after provider readback returns multiple values for a nested computed MaxItems=1 field
  • include the refresh cross-test helper and docs updates added during the investigation

Notes

  • this is a repro-only draft PR and does not propose a fix yet
  • the current understanding is that Terraform accepts provider-produced state that can contain multiple values here, while tfbridge panics when converting that state back into Pulumi outputs

Testing

  • go test ./pkg/tests -run TestUpdateMaxItemsOneComputedNullStateParity -count=1 -v
    • currently fails intentionally with Unexpected multiple elements in array with MaxItems=1

@codecov

codecov Bot commented Mar 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.05%. Comparing base (b09c0ff) to head (5964df6).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3390      +/-   ##
==========================================
- Coverage   70.05%   70.05%   -0.01%     
==========================================
  Files         342      342              
  Lines       37030    37030              
==========================================
- Hits        25943    25941       -2     
- Misses       9336     9337       +1     
- Partials     1751     1752       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review: repro test for MaxItems=1 update panic

This is a well-scoped, intentionally-failing draft PR. The test faithfully mimics the real GCP provider shape (nested computed MaxItems=1 fields, ignore_changes guard, cloud readback returning multiple policies) and the failure mode is reproducible and clearly documented.

No blocking issues found. All observations are non-blocking and are called out inline:

  1. docs/guides/testing.md references pkg/tests/refresh_cross_test.go, which doesn't exist on main or in this diff — likely needs updating or a note that the file is planned.
  2. pulCheckRecoveringReadPanics in refresh.go is missing the skipUnlessLinux(t) gate that pulcheck.PulCheck provides, creating asymmetric behaviour on non-Linux CI.
  3. The outer recoveringReadPanicsServer wrapper appears redundant because PanicRecoveringProviderServer (returned by ProviderServerFromInfo) already converts panics to gRPC errors before the outer defer/recover could fire.
  4. setPanicRecoveringProviderServerField is duplicated verbatim between refresh.go and update_cross_test.go, and both use unsafe.Pointer + reflection to write to private fields. A small testing-only constructor on PanicRecoveringProviderServer would make this statically safe and avoid the duplication.

These are all appropriate to address in the fix PR rather than here. Approving so progress is not blocked on the draft.

Reviewed by Internal Trusted PR Reviewer

To install this agentic workflow, run

gh aw add pulumi-labs/gh-aw-internal/.github/workflows/gh-aw-pr-review.md@99a309bf18258dbae98c8176ea10657e0b08deca

Comment thread docs/guides/testing.md
Examples worth consulting:

- Diff parity (`pkg/internal/tests/cross-tests/diff_cross_test.go`)
- Refresh regressions (`pkg/tests/refresh_cross_test.go`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: This line references pkg/tests/refresh_cross_test.go, but that file does not exist on main and is not among the files added by this PR. The existing file on main is pkg/tests/refresh_test.go. If the cross-test file is planned for a follow-up PR, please note that here; otherwise, point to the existing file to avoid misleading contributors.

}
}

func pulCheckRecoveringReadPanics(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: pulCheckRecoveringReadPanics is intended as a drop-in replacement for pulcheck.PulCheck when panic recovery is needed, but it is missing the skipUnlessLinux(t) call that pulcheck.PulCheck makes. On a non-Linux CI run, the normal Refresh path (when recoverReadPanics is false) will be skipped, but a call with RefreshRecoverReadPanics() will not be skipped, causing asymmetric test execution. Adding skipUnlessLinux(t) as the first statement (or a call to pulcheck.PulCheck with an option to customise the server) would close this gap.


handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{
Init: func(srv *grpc.Server) error {
pulumirpc.RegisterResourceProviderServer(srv, &recoveringReadPanicsServer{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: pulcheck.ProviderServerFromInfo already returns a *providerserver.PanicRecoveringProviderServer, which wraps every method (including Read) with its own defer/recover. Registering &recoveringReadPanicsServer{ResourceProviderServer: prov} around it means a Read panic would be caught by the inner wrapper and turned into a gRPC error long before the outer defer/recover in recoveringReadPanicsServer.Read fires. The outer wrapper appears redundant. If there is a path where a panic can escape the inner server, that should be explained in a comment; otherwise the outer layer can be removed.

setPanicRecoveringProviderServerField(t, wrapped, "omitStackTraces", true)
}

func setPanicRecoveringProviderServerField(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: setPanicRecoveringProviderServerField (and prepareProviderForRecoveredReadPanics) use unsafe.Pointer + reflect.NewAt to write to unexported fields of PanicRecoveringProviderServer. This creates a hidden compile-time-invisible coupling: renaming either logger or omitStackTraces would cause a silent runtime panic rather than a build failure. Consider surfacing a testing-only constructor or option on PanicRecoveringProviderServer (e.g., NewForTest(inner, logger, omitStackTraces)) to make this dependency explicit and statically checked. The same pattern is duplicated in pkg/tests/update_cross_test.go; centralising it would halve the surface area that needs updating when the struct changes.

)
}

func prepareProviderForBridgePanics(t *testing.T, server pulumirpc.ResourceProviderServer) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: setPanicRecoveringProviderServerField and prepareProviderForBridgePanics here are nearly line-for-line copies of the same helpers in pkg/internal/tests/cross-tests/refresh.go (the only difference is *testing.T vs the T interface). When the fix PR lands and this helper is needed in more than two places, extracting it to a shared internal package (e.g., pkg/internal/tests/pulcheck) would prevent them from drifting.

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.

1 participant