From d3b2dedd1dd7becb7b3b3bad902ecf9e4aed84b1 Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Mon, 15 Nov 2021 10:12:12 -0800 Subject: [PATCH] [sdk/python] Unmarshal output values in component providers (#8212) --- CHANGELOG_PENDING.md | 3 + sdk/python/lib/pulumi/provider/server.py | 47 +- sdk/python/lib/pulumi/runtime/known_types.py | 2 +- .../lib/pulumi/runtime/proto/provider_pb2.pyi | 5 +- sdk/python/lib/test/provider/test_server.py | 514 +++++++++++++++++- sdk/python/lib/test/test_next_serialize.py | 4 +- 6 files changed, 552 insertions(+), 23 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f9e9f2789..1aa9185c6 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -11,6 +11,9 @@ - [sdk/dotnet] - Marshal output values. [#8316](https://github.com/pulumi/pulumi/pull/8316) +- [sdk/python] - Unmarshal output values in component provider. + [#8212](https://github.com/pulumi/pulumi/pull/8212) + ### Bug Fixes - [engine] - Compute dependents correctly during targeted deletes. diff --git a/sdk/python/lib/pulumi/provider/server.py b/sdk/python/lib/pulumi/provider/server.py index cf2023e21..b24dd5af8 100644 --- a/sdk/python/lib/pulumi/provider/server.py +++ b/sdk/python/lib/pulumi/provider/server.py @@ -25,6 +25,7 @@ import sys import grpc import grpc.aio +from google.protobuf import struct_pb2 from pulumi.provider.provider import Provider, CallResult, ConstructResult from pulumi.resource import ProviderResource, Resource, DependencyResource, DependencyProviderResource, \ _parse_resource_reference @@ -81,8 +82,7 @@ class ProviderServicer(ResourceProviderServicer): preview=request.dryRun) pulumi.runtime.config.set_all_config(dict(request.config), request.configSecretKeys) - - inputs = await self._construct_inputs(request) + inputs = await self._construct_inputs(request.inputs, request.inputDependencies) result = self.provider.construct(name=request.name, resource_type=request.type, @@ -102,28 +102,32 @@ class ProviderServicer(ResourceProviderServicer): return response @staticmethod - async def _construct_inputs(request: proto.ConstructRequest) -> Dict[str, pulumi.Input[Any]]: + async def _construct_inputs(inputs: struct_pb2.Struct, input_dependencies: Any) -> Dict[str, pulumi.Input[Any]]: def deps(key: str) -> Set[str]: return set(urn for urn in - request.inputDependencies.get( + input_dependencies.get( key, proto.ConstructRequest.PropertyDependencies() ).urns) return { - k: await ProviderServicer._create_output(the_input, deps=deps(k)) + k: await ProviderServicer._select_value(the_input, deps=deps(k)) for k, the_input in - rpc.deserialize_properties(request.inputs, keep_unknowns=True).items() + rpc.deserialize_properties(inputs, keep_unknowns=True).items() } @staticmethod - async def _create_output(the_input: Any, deps: Set[str]) -> Any: + async def _select_value(the_input: Any, deps: Set[str]) -> Any: is_secret = rpc.is_rpc_secret(the_input) - # If it's a resource reference or a prompt value, return it directly without wrapping - # it as an output. - if await _is_resource_reference(the_input, deps) or (not is_secret and len(deps) == 0): + # If the input isn't a secret and either doesn't have any dependencies, already contains Outputs (from + # deserialized output values), or is a resource reference, then return it directly without wrapping it + # as an output. + if not is_secret and ( + len(deps) == 0 or + _contains_outputs(the_input) or + await _is_resource_reference(the_input, deps)): return the_input # Otherwise, wrap it as an output so we can handle secrets @@ -220,7 +224,7 @@ class ProviderServicer(ResourceProviderServicer): ).urns) return { - k: await ProviderServicer._create_output(the_input, deps=deps(k)) + k: await ProviderServicer._select_value(the_input, deps=deps(k)) for k, the_input in # We need to keep_internal, to keep the `__self__` that would normally be filtered because # it starts with "__". @@ -249,7 +253,7 @@ class ProviderServicer(ResourceProviderServicer): return proto.CallResponse(**resp) async def Configure(self, request, context) -> proto.ConfigureResponse: # pylint: disable=invalid-overridden-method - return proto.ConfigureResponse(acceptSecrets=True, acceptResources=True) + return proto.ConfigureResponse(acceptSecrets=True, acceptResources=True, acceptOutputs=True) async def GetPluginInfo(self, request, context) -> proto.PluginInfo: # pylint: disable=invalid-overridden-method return proto.PluginInfo(version=self.provider.version) @@ -331,6 +335,25 @@ async def _is_resource_reference(the_input: Any, deps: Set[str]) -> bool: and next(iter(deps)) == await cast(Resource, the_input).urn.future()) +def _contains_outputs(the_input: Any) -> bool: + """ + Returns true if the input contains Outputs (deeply). + """ + if known_types.is_output(the_input): + return True + + if isinstance(the_input, list): + for e in the_input: + if _contains_outputs(e): + return True + elif isinstance(the_input, dict): + for k in the_input: + if _contains_outputs(the_input[k]): + return True + + return False + + def _create_provider_resource(ref: str) -> ProviderResource: """ Rehydrate the provider reference into a registered ProviderResource, diff --git a/sdk/python/lib/pulumi/runtime/known_types.py b/sdk/python/lib/pulumi/runtime/known_types.py index b3f5b3345..f6549fc09 100644 --- a/sdk/python/lib/pulumi/runtime/known_types.py +++ b/sdk/python/lib/pulumi/runtime/known_types.py @@ -1,4 +1,4 @@ -# Copyright 2016-2021, Pulumi Corporation. +# Copyright 2016-2018, Pulumi Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/sdk/python/lib/pulumi/runtime/proto/provider_pb2.pyi b/sdk/python/lib/pulumi/runtime/proto/provider_pb2.pyi index 3d0d5b3cc..7de75f2f7 100644 --- a/sdk/python/lib/pulumi/runtime/proto/provider_pb2.pyi +++ b/sdk/python/lib/pulumi/runtime/proto/provider_pb2.pyi @@ -114,12 +114,13 @@ class ConfigureResponse: def __init__(self, acceptSecrets: bool=False, supportsPreview: bool=False, - acceptResources: bool=False) -> None: ... + acceptResources: bool=False, + acceptOutputs: bool=False) -> None: ... acceptSecrets: bool supportsPreview: bool acceptResources: bool - + acceptOutputs: bool class GetSchemaRequest: version: int diff --git a/sdk/python/lib/test/provider/test_server.py b/sdk/python/lib/test/provider/test_server.py index d3d3828ec..35a187ec5 100644 --- a/sdk/python/lib/test/provider/test_server.py +++ b/sdk/python/lib/test/provider/test_server.py @@ -12,24 +12,42 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, Any +import functools +from typing import Dict, Any, Optional, Tuple, List, Set, Callable, Awaitable +from semver import VersionInfo as Version -import os import pytest +from pulumi.runtime.settings import Settings, configure from pulumi.provider.server import ProviderServicer -from pulumi.runtime import proto, rpc +from pulumi.runtime import proto, rpc, rpc_manager, ResourceModule, Mocks +from pulumi.resource import CustomResource, ResourceOptions from pulumi.runtime.proto.provider_pb2 import ConstructRequest -import google.protobuf.struct_pb2 as struct_pb2 +from google.protobuf import struct_pb2 import pulumi.output +def pulumi_test(coro): + wrapped = pulumi.runtime.test(coro) + + @functools.wraps(wrapped) + def wrapper(*args, **kwargs): + configure(Settings()) + rpc._RESOURCE_PACKAGES.clear() + rpc._RESOURCE_MODULES.clear() + rpc_manager.RPC_MANAGER = rpc_manager.RPCManager() + + wrapped(*args, **kwargs) + + return wrapper + + @pytest.mark.asyncio async def test_construct_inputs_parses_request(): value = 'foobar' inputs = _as_struct({'echo': value}) req = ConstructRequest(inputs=inputs) - inputs = await ProviderServicer._construct_inputs(req) + inputs = await ProviderServicer._construct_inputs(req.inputs, req.inputDependencies) # pylint: disable=no-member assert len(inputs) == 1 assert inputs['echo'] == value @@ -39,7 +57,7 @@ async def test_construct_inputs_preserves_unknowns(): unknown = '04da6b54-80e4-46f7-96ec-b56ff0331ba9' inputs = _as_struct({'echo': unknown}) req = ConstructRequest(inputs=inputs) - inputs = await ProviderServicer._construct_inputs(req) + inputs = await ProviderServicer._construct_inputs(req.inputs, req.inputDependencies) # pylint: disable=no-member assert len(inputs) == 1 assert isinstance(inputs['echo'], pulumi.output.Unknown) @@ -48,3 +66,487 @@ def _as_struct(key_values: Dict[str, Any]) -> struct_pb2.Struct: the_struct = struct_pb2.Struct() the_struct.update(key_values) # pylint: disable=no-member return the_struct + +class MockResource(CustomResource): + def __init__(self, name: str, opts: Optional[ResourceOptions] = None): + CustomResource.__init__(self, "test:index:MockResource", name, opts=opts) + +class MockInputDependencies: + """ A mock for ConstructRequest.inputDependencies + + We need only support a `get() -> T where T.urns: List[str]` operation. + """ + def __init__(self, urns: Optional[List[str]]): + self.urns = urns if urns else [] + + def get(self, *args): + #pylint: disable=unused-argument + return self + +class TestModule(ResourceModule): + def construct(self, name: str, typ: str, urn: str): + if typ == "test:index:MockResource": + return MockResource(name, opts=ResourceOptions(urn=urn)) + raise Exception(f"unknown resource type {typ}") + + def version(self) -> Optional[Version]: + return None + +class TestMocks(Mocks): + def call(self, args: pulumi.runtime.MockCallArgs) -> Any: + raise Exception(f"unknown function {args.token}") + + def new_resource(self, args: pulumi.runtime.MockResourceArgs) -> Tuple[Optional[str], dict]: + return args.name+"_id", args.inputs + +def assert_output_equal(value: Any, + known: bool, secret: bool, + deps: Optional[List[str]] = None): + async def check(actual: Any): + assert isinstance(actual, pulumi.Output) + + if callable(value): + res = value(await actual.future()) + if isinstance(res, Awaitable): + await res + else: + assert (await actual.future()) == value + + assert known == await actual.is_known() + assert secret == await actual.is_secret() + + actual_deps: Set[Optional[str]] = set() + resources = await actual.resources() + for r in resources: + urn = await r.urn.future() + actual_deps.add(urn) + + assert actual_deps == set(deps if deps else []) + return True + return check + + +def create_secret(value: Any): + return {rpc._special_sig_key: rpc._special_secret_sig, "value": value} + +def create_resource_ref(urn: str, id_: Optional[str]): + ref = {rpc._special_sig_key: rpc._special_resource_sig, "urn": urn} + if id_ is not None: + ref["id"] = id_ + return ref + +def create_output_value(value: Optional[Any] = None, + secret: Optional[bool] = None, + dependencies: Optional[List[str]] = None): + val: Dict[str, Any] = {rpc._special_sig_key: rpc._special_output_value_sig} + if value is not None: + val["value"] = value + if secret is not None: + val["secret"] = secret + if dependencies is not None: + val["dependencies"] = dependencies + return val + +test_urn = "urn:pulumi:stack::project::test:index:MockResource::name" +test_id = "name_id" + +class UnmarshalOutputTestCase: + def __init__(self, + name: str, + input_: Any, + deps: Optional[List[str]] = None, + expected: Optional[Any] = None, + assert_: Optional[Callable[[Any], Awaitable]] = None): + self.name = name + self.input_ = input_ + self.deps = deps + self.expected = expected + self.assert_ = assert_ + + async def run(self): + pulumi.runtime.set_mocks(TestMocks(), "project", "stack", True) + pulumi.runtime.register_resource_module("test", "index", TestModule()) + # This registers the resource purely for the purpose of the test. + pulumi.runtime.settings.get_monitor().resources[test_urn] = \ + pulumi.runtime.mocks.MockMonitor.ResourceRegistration(test_urn, test_id, dict()) + + inputs = { "value": self.input_ } + input_struct = _as_struct(inputs) + req = ConstructRequest(inputs=input_struct) + result = await ProviderServicer._construct_inputs( + req.inputs, MockInputDependencies(self.deps)) # pylint: disable=no-member + actual = result["value"] + if self.assert_: + await self.assert_(actual) + else: + assert actual == self.expected + + +class Assert: + """Describes a series of asserts to be performed. + + Each assert can be: + - An async value to be awaited and asserted. + assert await val + + - A sync function to be called and asserted on. This will be called on the + same set of arguments that the class was called on. + assert fn(actual) + + - A plain value to be asserted on. + assert val + + """ + def __init__(self, *asserts): + self.asserts = asserts + + async def __call__(self, *args, **kargs): + for assert_ in self.asserts: + assert await Assert.__eval(assert_, *args, **kargs) + + @staticmethod + async def __eval(a, *args, **kargs) -> Any: + if isinstance(a, Awaitable): + return await a + elif isinstance(a, Callable): + a_res = a(*args, **kargs) + return await Assert.__eval(a_res, *args, **kargs) + return a + + @staticmethod + def async_equal(a, b): + """Asserts that two values are equal when evaluated with async and + given the args that `Asserts` were called on. + """ + async def check(*args, **kargs): + a_res = await Assert.__eval(a, *args, **kargs) + b_res = await Assert.__eval(b, *args, **kargs) + assert a_res == b_res + return True + return check + +async def array_nested_resource_ref(actual): + async def helper(v: Any): + assert isinstance(v, list) + assert isinstance(v[0], MockResource) + assert await v[0].urn.future() == test_urn + assert await v[0].id.future() == test_id + await assert_output_equal(helper, True, False, [test_urn])(actual) + +async def object_nested_resource_ref(actual): + async def helper(v: Any): + assert isinstance(v["foo"], MockResource) + assert await v["foo"].urn.future() == test_urn + assert await v["foo"].id.future() == test_id + await assert_output_equal(helper, True, False, [test_urn])(actual) + +async def object_nested_resource_ref_and_secret(actual): + async def helper(v: Any): + assert isinstance(v["foo"], MockResource) + assert await v["foo"].urn.future() == test_urn + assert await v["foo"].id.future() == test_id + assert v["bar"] == "ssh" + await assert_output_equal(helper, True, True, [test_urn])(actual) + + +deserialization_tests = [ + UnmarshalOutputTestCase( + name="unknown", + input_=rpc.UNKNOWN, + deps=["fakeURN"], + assert_=assert_output_equal(None, False, False, ["fakeURN"]), + ), + UnmarshalOutputTestCase( + name="array nested unknown", + input_=[rpc.UNKNOWN], + deps=["fakeURN"], + assert_=assert_output_equal(None, False, False, ["fakeURN"]), + ), + UnmarshalOutputTestCase( + name="object nested unknown", + input_={"foo": rpc.UNKNOWN}, + deps=["fakeURN"], + assert_=assert_output_equal(None, False, False, ["fakeURN"]), + ), + UnmarshalOutputTestCase( + name="unknown output value", + input_=create_output_value(None, False, ["fakeURN"]), + deps=["fakeURN"], + assert_=assert_output_equal(None, False, False, ["fakeURN"]), + ), + UnmarshalOutputTestCase( + name="unknown output value (no deps)", + input_=create_output_value(), + assert_=assert_output_equal(None, False, False), + ), + UnmarshalOutputTestCase( + name="array nested unknown output value", + input_=[create_output_value(None, False, ["fakeURN"])], + deps=["fakeURN"], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual[0]) + ), + ), + UnmarshalOutputTestCase( + name="array nested unknown output value (no deps)", + input_=[create_output_value(None, False, ["fakeURN"])], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual[0]) + ), + ), + UnmarshalOutputTestCase( + name="object nested unknown output value", + input_= { "foo": create_output_value(None, False, ["fakeURN"]) }, + deps=["fakeURN"], + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual["foo"]), + ), + ), + UnmarshalOutputTestCase( + name="object nested unknown output value (no deps)", + input_= { "foo": create_output_value(None, False, ["fakeURN"]) }, + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual["foo"]), + ), + ), + UnmarshalOutputTestCase( + name="string value (no deps)", + input_="hi", + expected="hi", + ), + UnmarshalOutputTestCase( + name="array nested string value (no deps)", + input_=["hi"], + expected=["hi"], + ), + UnmarshalOutputTestCase( + name="object nested string value (no deps)", + input_= { "foo": "hi" }, + expected= { "foo": "hi" }, + ), + UnmarshalOutputTestCase( + name="string output value", + input_=create_output_value("hi", False, ["fakeURN"]), + deps=["fakeURN"], + assert_=assert_output_equal("hi", True, False, ["fakeURN"]), + ), + UnmarshalOutputTestCase( + name="string output value (no deps)", + input_=create_output_value("hi"), + assert_=assert_output_equal("hi", True, False), + ), + UnmarshalOutputTestCase( + name="array nested string output value", + input_=[create_output_value("hi", False, ["fakeURN"])], + deps=["fakeURN"], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual[0]), + ), + ), + UnmarshalOutputTestCase( + name="array nested string output value (no deps)", + input_=[create_output_value("hi", False, ["fakeURN"])], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual[0]), + ), + ), + UnmarshalOutputTestCase( + name="object nested string output value", + input_={ "foo": create_output_value("hi", False, ["fakeURN"])}, + deps=["fakeURN"], + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual["foo"]) + ), + ), + UnmarshalOutputTestCase( + name="object nested string output value (no deps)", + input_={ "foo": create_output_value("hi", False, ["fakeURN"])}, + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual["foo"]) + ), + ), + UnmarshalOutputTestCase( + name="string secrets (no deps)", + input_=create_secret("shh"), + assert_=assert_output_equal("shh", True, True), + ), + UnmarshalOutputTestCase( + name="array nested string secrets (no deps)", + input_=[create_secret("shh")], + assert_=assert_output_equal(["shh"], True, True), + ), + UnmarshalOutputTestCase( + name="object nested string secrets (no deps)", + input_={ "foo": create_secret("shh")}, + assert_=assert_output_equal({"foo": "shh"}, True, True), + ), + UnmarshalOutputTestCase( + name="string secret output value (no deps)", + input_=create_output_value("shh", True), + assert_=assert_output_equal("shh", True, True), + ), + UnmarshalOutputTestCase( + name="array nested string secret output value (no deps)", + input_=[create_output_value("shh", True)], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal("shh", True, True)(actual[0]), + ), + ), + UnmarshalOutputTestCase( + name="object nested string secret output value (no deps)", + input_={"foo": create_output_value("shh", True)}, + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal("shh", True, True)(actual["foo"]), + ), + ), + UnmarshalOutputTestCase( + name="string secret output value", + input_=create_output_value("shh", True, ["fakeURN1", "fakeURN2"]), + deps=["fakeURN1", "fakeURN2"], + assert_=assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"]), + ), + UnmarshalOutputTestCase( + name="string secret output value (no deps)", + input_=create_output_value("shh", True, ["fakeURN1", "fakeURN2"]), + assert_=assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"]), + ), + UnmarshalOutputTestCase( + name="array nested string secret output value", + input_=[create_output_value("shh", True, ["fakeURN1", "fakeURN2"])], + deps=["fakeURN1", "fakeURN2"], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual[0]), + ), + ), + UnmarshalOutputTestCase( + name="array nested string secret output value (no deps)", + input_=[create_output_value("shh", True, ["fakeURN1", "fakeURN2"])], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual[0]), + ), + ), + UnmarshalOutputTestCase( + name="object nested string secret output value", + input_={ "foo": create_output_value("shh", True, ["fakeURN1", "fakeURN2"])}, + deps=["fakeURN1", "fakeURN2"], + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual["foo"]), + ), + ), + UnmarshalOutputTestCase( + name="object nested string secret output value (no deps)", + input_={ "foo": create_output_value("shh", True, ["fakeURN1", "fakeURN2"])}, + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual["foo"]), + ), + ), + UnmarshalOutputTestCase( + name="resource ref", + input_=create_resource_ref(test_urn, test_id), + deps=[test_urn], + assert_=Assert( + lambda actual: isinstance(actual, MockResource), + Assert.async_equal(lambda actual: actual.urn.future(), test_urn), + Assert.async_equal(lambda actual: actual.id.future(), test_id), + ), + ), + UnmarshalOutputTestCase( + name="resource ref (no deps)", + input_=create_resource_ref(test_urn, test_id), + assert_=Assert( + lambda actual: isinstance(actual, MockResource), + Assert.async_equal(lambda actual: actual.urn.future(), test_urn), + Assert.async_equal(lambda actual: actual.id.future(), test_id), + ), + ), + UnmarshalOutputTestCase( + name="array nested resource ref", + input_=[create_resource_ref(test_urn, test_id)], + deps=[test_urn], + assert_=array_nested_resource_ref + ), + UnmarshalOutputTestCase( + name="array nested resource ref (no deps)", + input_=[create_resource_ref(test_urn, test_id)], + assert_=Assert( + lambda actual: isinstance(actual, list), + lambda actual: isinstance(actual[0], MockResource), + Assert.async_equal(lambda actual: actual[0].urn.future(), test_urn), + Assert.async_equal(lambda actual: actual[0].id.future(), test_id), + ), + ), + UnmarshalOutputTestCase( + name="object nested resource ref", + input_={ "foo": create_resource_ref(test_urn, test_id) }, + deps=[test_urn], + assert_=object_nested_resource_ref + ), + UnmarshalOutputTestCase( + name="object nested resource ref (no deps)", + input_={ "foo": create_resource_ref(test_urn, test_id) }, + assert_=Assert( + lambda actual: isinstance(actual["foo"], MockResource), + Assert.async_equal(lambda actual: actual["foo"].urn.future(), test_urn), + Assert.async_equal(lambda actual: actual["foo"].id.future(), test_id), + ), + ), + UnmarshalOutputTestCase( + name="object nested resource ref and secret", + input_={ + "foo": create_resource_ref(test_urn, test_id), + "bar": create_secret("ssh"), + }, + deps=[test_urn], + assert_=object_nested_resource_ref_and_secret + ), + UnmarshalOutputTestCase( + name="object nested resource ref and secret output value", + input_={ + "foo": create_resource_ref(test_urn, test_id), + "bar": create_output_value("shh", True), + }, + deps=[test_urn], + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: isinstance(actual["foo"], MockResource), + Assert.async_equal(lambda actual: actual["foo"].urn.future(), test_urn), + Assert.async_equal(lambda actual: actual["foo"].id.future(), test_id), + lambda actual: assert_output_equal("shh", True, True)(actual["bar"]), + ), + ), + UnmarshalOutputTestCase( + name="object nested resource ref and secret output value (no deps)", + input_={ + "foo": create_resource_ref(test_urn, test_id), + "bar": create_output_value("shh", True), + }, + assert_=Assert( + lambda actual: not isinstance(actual, pulumi.Output), + lambda actual: isinstance(actual["foo"], MockResource), + Assert.async_equal(lambda actual: actual["foo"].urn.future(), test_urn), + Assert.async_equal(lambda actual: actual["foo"].id.future(), test_id), + lambda actual: assert_output_equal("shh", True, True)(actual["bar"]), + ), + ), +] + + +@pytest.mark.parametrize( + "testcase", deserialization_tests, ids=list(map(lambda x: x.name, deserialization_tests))) +@pulumi_test +async def test_deserialize_correctly(testcase): + await testcase.run() diff --git a/sdk/python/lib/test/test_next_serialize.py b/sdk/python/lib/test/test_next_serialize.py index c41a1f61f..2a3ede359 100644 --- a/sdk/python/lib/test/test_next_serialize.py +++ b/sdk/python/lib/test/test_next_serialize.py @@ -179,7 +179,7 @@ class NextSerializationTests(unittest.TestCase): self.assertEqual(id, prop["id"]) res = rpc.deserialize_properties(prop) - self.assertTrue(isinstance(res, MyCustomResource)) + self.assertIsInstance(res, MyCustomResource) rpc._RESOURCE_MODULES.clear() res = rpc.deserialize_properties(prop) @@ -209,7 +209,7 @@ class NextSerializationTests(unittest.TestCase): self.assertEqual(id, prop["id"]) res = rpc.deserialize_properties(prop) - self.assertTrue(isinstance(res, MyCustomResource)) + self.assertIsInstance(res, MyCustomResource) rpc._RESOURCE_MODULES.clear() res = rpc.deserialize_properties(prop)