diff --git a/sdk/Pulumi.Tests/Mocks/Issue456.cs b/sdk/Pulumi.Tests/Mocks/Issue456.cs new file mode 100644 index 00000000..0ca75082 --- /dev/null +++ b/sdk/Pulumi.Tests/Mocks/Issue456.cs @@ -0,0 +1,86 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using OneOf.Types; +using Pulumi.Serialization; +using Pulumi.Testing; + +namespace Pulumi.Tests.Mocks +{ + /// + /// Supports testing that null returned for an InputMap doesn't cause a null reference exception. + /// + /// See https://github.com/pulumi/pulumi-dotnet/issues/456 + /// + public sealed class Issue456 + { + public class ReproStack : Stack + { + [Output("result")] + public Output Result { get; private set; } + + public ReproStack() + { + var instance1 = new CustomMap("instance1"); + var instance2 = new CustomMap("instance2"); + // Assert that both instance1 and instance2 don't have null for Metadata.Labels + + this.Result = Output.Tuple(instance1.Metadata, instance2.Metadata).Apply(tuple => { + if (tuple.Item1.Labels == null || tuple.Item2.Labels == null) { + throw new Exception("Labels should not be null"); + } + return "success"; + }); + } + } + + public class ReproMocks : IMocks + { + public Task CallAsync(MockCallArgs args) + { + throw new Exception("CallAsync should not be called"); + } + + public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args) + { + if (args.Type != "pkg:index:CustomMap") + { + throw new Exception($"Unknown resource {args.Type}"); + } + + if (args.Name == "instance1") { + return Task.FromResult<(string?, object)>( + ("some_id", + new Dictionary + { + { + "metadata", + new Dictionary + { + { "labels", null! }, + } + }, + } + )); + } else if (args.Name == "instance2") { + return Task.FromResult<(string?, object)>( + ("some_id", + new Dictionary + { + { + "metadata", + new Dictionary + { + } + }, + } + )); + } else { + throw new Exception($"Unknown resource {args.Name}"); + } + } + } + } +} diff --git a/sdk/Pulumi.Tests/Mocks/MocksTests.cs b/sdk/Pulumi.Tests/Mocks/MocksTests.cs index c3e75591..6926e152 100644 --- a/sdk/Pulumi.Tests/Mocks/MocksTests.cs +++ b/sdk/Pulumi.Tests/Mocks/MocksTests.cs @@ -367,6 +367,13 @@ public async Task TestAliases() } } } + + [Fact] + public async Task TestNullMaps() + { + await Deployment.TestAsync( + new Issue456.ReproMocks()); + } } public static class Testing diff --git a/sdk/Pulumi.Tests/Mocks/TestStack.cs b/sdk/Pulumi.Tests/Mocks/TestStack.cs index a857a68e..dd736649 100644 --- a/sdk/Pulumi.Tests/Mocks/TestStack.cs +++ b/sdk/Pulumi.Tests/Mocks/TestStack.cs @@ -1,5 +1,7 @@ // Copyright 2016-2020, Pulumi Corporation +using System.Collections.Immutable; + namespace Pulumi.Tests.Mocks { [ResourceType("aws:ec2/instance:Instance", null)] @@ -47,4 +49,28 @@ public MyStack() this.PublicIp = myInstance.PublicIp; } } + + [OutputType] + public sealed class ObjectMeta + { + public readonly ImmutableDictionary Labels; + + [OutputConstructor] + private ObjectMeta(ImmutableDictionary labels){ + Labels = labels; + } + } + + + public class CustomMap : CustomResource + { + [Output("metadata")] + public Output Metadata { get; private set; } = null!; + + public CustomMap(string name, CustomResourceOptions? options = null) + : base("pkg:index:CustomMap", name, null, options) + { + } + } + } diff --git a/sdk/Pulumi/Core/InputMap.cs b/sdk/Pulumi/Core/InputMap.cs index f2e842c5..9f51fd74 100644 --- a/sdk/Pulumi/Core/InputMap.cs +++ b/sdk/Pulumi/Core/InputMap.cs @@ -154,7 +154,7 @@ public static implicit operator InputMap(Output> values public static implicit operator InputMap(Output> values) => values.Apply(ImmutableDictionary.CreateRange); - public static implicit operator InputMap(Output> values) + public static implicit operator InputMap(Output>values) => new InputMap(values.Apply(values => { var builder = ImmutableDictionary.CreateBuilder>();