[sdk/{nodejs,python}] Fix errors when testing remote components with mocks (#8053)

v3.13.0 introduces support for serializing outputs in inputs as special output value objects in the Node.js and Python SDKs when serializing inputs for remote components and method calls. This functionality is currently disabled by default in the engine (setting the `PULUMI_ENABLE_OUTPUT_VALUES` envvar to a truthy value enables it).

However, unit testing remote components with mocks results in errors being raised in v3.13.0, related to the new output value support. This is due to the mock monitor implementation saying it supports all features (which now includes output values), so the SDK serializers are serializing outputs as output values, which the mock monitor can't handle correctly.

This change addresses the issue by updating the mock monitor implementation in the Node.js and Python SDKs to indicate the specific features that are supported, excluding support for output values. New tests with mocks fail before the change and pass after.
This commit is contained in:
Justin Van Patten 2021-09-24 06:08:13 -07:00 committed by GitHub
parent b8b6f3dd70
commit 1e09626bc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 67 additions and 4 deletions

View file

@ -13,3 +13,6 @@
- [codegen/go] - Fix resolution of enum naming collisions
[#7985](https://github.com/pulumi/pulumi/pull/7985)
- [sdk/{nodejs,python}] - Fix errors when testing remote components with mocks.
[#8053](https://github.com/pulumi/pulumi/pull/8053)

View file

@ -206,8 +206,14 @@ export class MockMonitor {
}
public supportsFeature(req: any, callback: (err: any, innerResponse: any) => void) {
const id = req.getId();
// Support for "outputValues" is deliberately disabled for the mock monitor so
// instances of `Output` don't show up in `MockResourceArgs` inputs.
const hasSupport = id === "secrets" || id === "resourceReferences";
callback(null, {
getHassupport: () => true,
getHassupport: () => hasSupport,
});
}
}

View file

@ -42,6 +42,14 @@ pulumi.runtime.setMocks({
case "pkg:index:MyCustom":
assert.strictEqual(args.custom, true);
return { id: args.name + "_id", state: args.inputs };
case "pkg:index:MyRemoteComponent":
return {
id: `${args.name}_id`,
state: {
...args.inputs,
outprop: `output: ${args.inputs["inprop"]}`,
},
};
default:
assert.strictEqual(args.custom, false);
return { id: "", state: {} };
@ -57,6 +65,13 @@ class MyComponent extends pulumi.ComponentResource {
}
}
class MyRemoteComponent extends pulumi.ComponentResource {
outprop!: pulumi.Output<string>;
constructor(name: string, inprop: pulumi.Input<string>, opts?: pulumi.ComponentResourceOptions) {
super("pkg:index:MyRemoteComponent", name, { inprop, outprop: undefined }, opts, true);
}
}
class Instance extends pulumi.CustomResource {
publicIP!: pulumi.Output<string>;
constructor(name: string, opts?: CustomResourceOptions) {
@ -92,6 +107,7 @@ const mycomponent = new MyComponent("mycomponent", "hello");
const myinstance = new Instance("instance");
const mycustom = new MyCustom("mycustom", { instance: myinstance });
const invokeResult = invoke();
const myremotecomponent = new MyRemoteComponent("myremotecomponent", pulumi.interpolate`hello: ${myinstance.id}`);
describe("mocks", function() {
describe("component", function() {
@ -103,6 +119,15 @@ describe("mocks", function() {
});
});
describe("remote component", function() {
it("has expected output value", done => {
myremotecomponent.outprop.apply(outprop => {
assert.strictEqual(outprop.startsWith("output: hello: "), true);
done();
});
});
});
describe("custom", function() {
it("instance has expected output value", done => {
myinstance.publicIP.apply(ip => {

View file

@ -226,8 +226,10 @@ class MockMonitor:
return empty_pb2.Empty()
def SupportsFeature(self, request):
# pylint: disable=unused-argument
return type('SupportsFeatureResponse', (object,), {'hasSupport' : True})
# Support for "outputValues" is deliberately disabled for the mock monitor so
# instances of `Output` don't show up in `MockResourceArgs` inputs.
has_support = request.id in {"secrets", "resourceReferences"}
return type('SupportsFeatureResponse', (object,), {'hasSupport' : has_support})
class MockEngine:

View file

@ -26,6 +26,17 @@ class MyComponent(pulumi.ComponentResource):
self.outprop = pulumi.Output.from_input(inprop).apply(lambda x: f"output: {x}")
class MyRemoteComponent(pulumi.ComponentResource):
outprop: pulumi.Output[str]
def __init__(self, name, inprop: pulumi.Input[str] = None, opts = None):
if inprop is None:
raise TypeError("Missing required property 'inprop'")
__props__: dict = dict()
__props__["inprop"] = inprop
__props__["outprop"] = None
super().__init__("pkg:index:MyRemoteComponent", name, __props__, opts, True)
class Instance(pulumi.CustomResource):
public_ip: pulumi.Output[str]
def __init__(self, resource_name, name: pulumi.Input[str] = None, value: pulumi.Input[str] = None, opts = None):
@ -69,6 +80,7 @@ def define_resources():
value=pulumi.Output.secret("secret_value"))
mycustom = MyCustom("mycustom", {"instance": myinstance})
invoke_result = do_invoke()
myremotecomponent = MyRemoteComponent("myremotecomponent", inprop=myinstance.id.apply(lambda v: f"hello: {v}"))
# Pass myinstance several more times to ensure deserialization of the resource reference
# works on other asyncio threads.
@ -86,7 +98,8 @@ def define_resources():
'myinstance': myinstance,
'mycustom': mycustom,
'dns_ref': dns_ref,
'invoke_result': invoke_result
'invoke_result': invoke_result,
'myremotecomponent': myremotecomponent,
}

View file

@ -44,6 +44,15 @@ def test_component(my_resources):
return my_resources['mycomponent'].outprop.apply(check_outprop)
@pulumi.runtime.test
def test_remote_component(my_resources):
def check_outprop(outprop):
assert outprop.startswith("output: hello: ")
return my_resources['myremotecomponent'].outprop.apply(check_outprop)
@pulumi.runtime.test
def test_custom(my_resources):
@ -152,6 +161,11 @@ class MyMocks(pulumi.runtime.Mocks):
return [args.name + '_id', args.inputs]
elif args.typ == 'pulumi:pulumi:StackReference' and 'dns' in args.name:
return [args.name, {'outputs': {'haha': 'business'}}]
elif args.typ == 'pkg:index:MyRemoteComponent':
state = {
'outprop': f"output: {args.inputs['inprop']}",
}
return [args.name + '_id', dict(args.inputs, **state)]
else:
return ['', {}]