Skip to content

Commit 577060f

Browse files
bicep deploy/bicep local-deploy - support for --format argument (#18096)
* Add new `--format` argument for the `deploy` and `local-deploy` commands * Add tests for new deployment commands
1 parent 83b590d commit 577060f

26 files changed

+952
-253
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.RegularExpressions;
5+
using Azure;
6+
using Azure.Core;
7+
using Azure.ResourceManager;
8+
using Azure.ResourceManager.Resources;
9+
using Azure.ResourceManager.Resources.Mocking;
10+
using Azure.ResourceManager.Resources.Models;
11+
using Bicep.Cli.Helpers.Deploy;
12+
using Bicep.Cli.UnitTests;
13+
using Bicep.Cli.UnitTests.Assertions;
14+
using Bicep.Cli.UnitTests.Helpers.Deploy;
15+
using Bicep.Core.AzureApi;
16+
using Bicep.Core.Configuration;
17+
using Bicep.Core.UnitTests;
18+
using Bicep.Core.UnitTests.Assertions;
19+
using Bicep.Core.UnitTests.Mock;
20+
using Bicep.Core.UnitTests.Utils;
21+
using FluentAssertions;
22+
using Microsoft.Extensions.DependencyInjection;
23+
using Microsoft.VisualStudio.TestTools.UnitTesting;
24+
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
25+
using Moq;
26+
using Newtonsoft.Json.Linq;
27+
28+
namespace Bicep.Cli.IntegrationTests.Commands;
29+
30+
[TestClass]
31+
public class DeployCommandTests : TestBase
32+
{
33+
public async Task<CliResult> Deploy(IDeploymentProcessor deploymentProcessor, string[]? additionalArgs = null)
34+
{
35+
additionalArgs ??= [];
36+
37+
var bicepparamsPath = FileHelper.SaveResultFile(
38+
TestContext,
39+
"main.bicepparam",
40+
"""
41+
var subscriptionId = readEnvVar('AZURE_SUBSCRIPTION_ID')
42+
var resourceGroup = readEnvVar('AZURE_RESOURCE_GROUP')
43+
44+
using './main.bicep' with {
45+
mode: 'deployment'
46+
scope: '/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}'
47+
}
48+
49+
param stgName = 'asiudfndaisud'
50+
""");
51+
52+
FileHelper.SaveResultFile(
53+
TestContext,
54+
"main.bicep",
55+
"""
56+
param stgName string
57+
58+
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
59+
name: stgName
60+
location: 'West US'
61+
kind: 'StorageV2'
62+
sku: {
63+
name: 'Standard_LRS'
64+
}
65+
}
66+
67+
output blobUri string = stg.properties.primaryEndpoints.blob
68+
""",
69+
Path.GetDirectoryName(bicepparamsPath));
70+
71+
var subscriptionId = Guid.NewGuid().ToString();
72+
var resourceGroup = "testRg";
73+
var settings = CreateDefaultSettings() with
74+
{
75+
Environment = TestEnvironment.Default.WithVariables(
76+
("AZURE_SUBSCRIPTION_ID", subscriptionId),
77+
("AZURE_RESOURCE_GROUP", resourceGroup)),
78+
FeatureOverrides = CreateDefaultFeatureProviderOverrides() with { DeployCommandsEnabled = true }
79+
};
80+
81+
return await Bicep(
82+
settings,
83+
services => services.AddSingleton(deploymentProcessor),
84+
TestContext.CancellationTokenSource.Token,
85+
["deploy", bicepparamsPath, ..additionalArgs]);
86+
}
87+
88+
[TestMethod]
89+
public async Task Deploy_should_succeed()
90+
{
91+
var deploymentProcessor = StrictMock.Of<IDeploymentProcessor>();
92+
deploymentProcessor.Setup(x => x.Deploy(It.IsAny<RootConfiguration>(), It.IsAny<DeployCommandsConfig>(), It.IsAny<Action<DeploymentWrapperView>>(), It.IsAny<CancellationToken>()))
93+
.Returns<RootConfiguration, DeployCommandsConfig, Action<DeploymentWrapperView>, CancellationToken>((_, config, onUpdate, _) =>
94+
{
95+
onUpdate(DeploymentRendererTests.Create(DateTime.UtcNow));
96+
97+
return Task.CompletedTask;
98+
});
99+
100+
var result = await Deploy(deploymentProcessor.Object);
101+
102+
result.WithoutAnsi().WithoutDurations().Stdout.Should().BeEquivalentToIgnoringNewlines("""
103+
╭──────────────┬──────────┬─────────────────╮
104+
│ Resource │ Duration │ Status │
105+
├──────────────┼──────────┼─────────────────┤
106+
│ blah │ │ Succeeded │
107+
│ fooNested │ │ Succeeded │
108+
│ fooNestedErr │ │ Oh dear oh dear │
109+
╰──────────────┴──────────┴─────────────────╯
110+
╭─────────┬────────╮
111+
│ Output │ Value │
112+
├─────────┼────────┤
113+
│ output1 │ value1 │
114+
│ output2 │ 42 │
115+
╰─────────┴────────╯
116+
117+
""");
118+
}
119+
120+
[TestMethod]
121+
public async Task Deploy_should_succeed_with_json_output()
122+
{
123+
var deploymentProcessor = StrictMock.Of<IDeploymentProcessor>();
124+
deploymentProcessor.Setup(x => x.Deploy(It.IsAny<RootConfiguration>(), It.IsAny<DeployCommandsConfig>(), It.IsAny<Action<DeploymentWrapperView>>(), It.IsAny<CancellationToken>()))
125+
.Returns<RootConfiguration, DeployCommandsConfig, Action<DeploymentWrapperView>, CancellationToken>((_, config, onUpdate, _) =>
126+
{
127+
onUpdate(DeploymentRendererTests.Create(DateTime.UtcNow));
128+
129+
return Task.CompletedTask;
130+
});
131+
132+
var result = await Deploy(deploymentProcessor.Object, ["--format", "json"]);
133+
134+
result.Stdout.Should().DeepEqualJson("""
135+
{
136+
"outputs": {
137+
"output2": 42,
138+
"output1": "value1"
139+
}
140+
}
141+
""");
142+
}
143+
144+
[TestMethod]
145+
public async Task Errors_are_displayed()
146+
{
147+
var deploymentProcessor = StrictMock.Of<IDeploymentProcessor>();
148+
deploymentProcessor.Setup(x => x.Deploy(It.IsAny<RootConfiguration>(), It.IsAny<DeployCommandsConfig>(), It.IsAny<Action<DeploymentWrapperView>>(), It.IsAny<CancellationToken>()))
149+
.Returns<RootConfiguration, DeployCommandsConfig, Action<DeploymentWrapperView>, CancellationToken>((_, config, onUpdate, _) =>
150+
{
151+
onUpdate(new(null, "Deployment failed"));
152+
153+
return Task.CompletedTask;
154+
});
155+
156+
var result = await Deploy(deploymentProcessor.Object);
157+
158+
result.WithoutAnsi().WithoutDurations().Stdout.Should().BeEquivalentToIgnoringNewlines("""
159+
╭╮
160+
161+
╰╯
162+
╭───────────────────╮
163+
│ Error │
164+
├───────────────────┤
165+
│ Deployment failed │
166+
╰───────────────────╯
167+
168+
""");
169+
}
170+
}

src/Bicep.Cli.IntegrationTests/LocalDeployCommandTests.cs renamed to src/Bicep.Cli.IntegrationTests/Commands/LocalDeployCommandTests.cs

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@
3232
using Newtonsoft.Json.Linq;
3333
using StreamJsonRpc;
3434

35-
namespace Bicep.Cli.IntegrationTests;
35+
namespace Bicep.Cli.IntegrationTests.Commands;
3636

3737
[TestClass]
38-
[Ignore("Commented out temporarily to investigate ANSI assertion differences in CI")]
3938
public class LocalDeployCommandTests : TestBase
4039
{
4140
private static ExtensionPackage GetMockLocalDeployPackage(BinaryData? tgzData = null)
@@ -65,12 +64,8 @@ private static void RegisterExtensionMocks(
6564
.AddSingleton(armDeploymentProvider);
6665
}
6766

68-
[TestMethod]
69-
public async Task Local_deploy_should_succeed()
67+
private ILocalExtension GetExtensionMock()
7068
{
71-
var paramFile = new EmbeddedFile(typeof(LocalDeployCommandTests).Assembly, "Files/LocalDeployCommandTests/weather/main.bicepparam");
72-
var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, paramFile);
73-
7469
var extensionMock = StrictMock.Of<ILocalExtension>();
7570
extensionMock.Setup(x => x.CreateOrUpdate(It.IsAny<ResourceSpecification>(), It.IsAny<CancellationToken>()))
7671
.Returns<ResourceSpecification, CancellationToken>((req, _) =>
@@ -118,25 +113,32 @@ public async Task Local_deploy_should_succeed()
118113
return Task.FromResult(new LocalExtensionOperationResponse(new Resource(req.Type, req.ApiVersion, req.Properties, (outputProperties as JsonObject)!, "Succeeded"), null));
119114
});
120115

116+
return extensionMock.Object;
117+
}
118+
119+
[TestMethod]
120+
public async Task Local_deploy_should_succeed()
121+
{
122+
var paramFile = new EmbeddedFile(typeof(LocalDeployCommandTests).Assembly, "Files/LocalDeployCommandTests/weather/main.bicepparam");
123+
var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, paramFile);
121124

122125
var services = await ExtensionTestHelper.GetServiceBuilderWithPublishedExtension(GetMockLocalDeployPackage(), new(LocalDeployEnabled: true));
123126
var clientFactory = services.Build().Construct<IContainerRegistryClientFactory>();
124127

125128
var result = await Bicep(
126129
new InvocationSettings(ClientFactory: clientFactory),
127-
services => RegisterExtensionMocks(services, extensionMock.Object),
130+
services => RegisterExtensionMocks(services, GetExtensionMock()),
128131
TestContext.CancellationTokenSource.Token,
129132
["local-deploy", baselineFolder.EntryFile.OutputFilePath]);
130133

131134
result.Should().NotHaveStderr().And.Succeed();
132-
var output = GetOutputWithoutDurations(result.Stdout);
133135

134-
output.Should().EqualIgnoringWhitespace("""
136+
result.WithoutAnsi().WithoutDurations().Stdout.Should().BeEquivalentToIgnoringNewlines("""
135137
╭───────────────┬──────────┬───────────╮
136138
│ Resource │ Duration │ Status │
137139
├───────────────┼──────────┼───────────┤
138-
│ gridpointsReq │ <snip> │ Succeeded │
139-
│ forecastReq │ <snip> │ Succeeded │
140+
│ gridpointsReq │ │ Succeeded │
141+
│ forecastReq │ │ Succeeded │
140142
╰───────────────┴──────────┴───────────╯
141143
╭────────────────┬────────────────────────────────╮
142144
│ Output │ Value │
@@ -157,6 +159,47 @@ public async Task Local_deploy_should_succeed()
157159
│ │ ] │
158160
│ forecastString │ Forecast: Name │
159161
╰────────────────┴────────────────────────────────╯
162+
163+
""");
164+
}
165+
166+
[TestMethod]
167+
public async Task Local_deploy_should_succeed_with_json_output()
168+
{
169+
var paramFile = new EmbeddedFile(typeof(LocalDeployCommandTests).Assembly, "Files/LocalDeployCommandTests/weather/main.bicepparam");
170+
var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, paramFile);
171+
172+
var services = await ExtensionTestHelper.GetServiceBuilderWithPublishedExtension(GetMockLocalDeployPackage(), new(LocalDeployEnabled: true));
173+
var clientFactory = services.Build().Construct<IContainerRegistryClientFactory>();
174+
175+
var result = await Bicep(
176+
new InvocationSettings(ClientFactory: clientFactory),
177+
services => RegisterExtensionMocks(services, GetExtensionMock()),
178+
TestContext.CancellationTokenSource.Token,
179+
["local-deploy", baselineFolder.EntryFile.OutputFilePath, "--format", "json"]);
180+
181+
result.Should().NotHaveStderr().And.Succeed();
182+
183+
result.Stdout.Should().DeepEqualJson("""
184+
{
185+
"outputs": {
186+
"forecast": [
187+
{
188+
"name": "Tonight",
189+
"temperature": 47
190+
},
191+
{
192+
"name": "Wednesday",
193+
"temperature": 64
194+
},
195+
{
196+
"name": "Wednesday Night",
197+
"temperature": 46
198+
}
199+
],
200+
"forecastString": "Forecast: Name"
201+
}
202+
}
160203
""");
161204
}
162205

@@ -233,13 +276,13 @@ public async Task Local_deploy_with_azure_should_succeed(bool async)
233276
["local-deploy", baselineFolder.EntryFile.OutputFilePath]);
234277

235278
result.Should().NotHaveStderr().And.Succeed();
236-
var output = GetOutputWithoutDurations(result.Stdout);
237-
output.Should().EqualIgnoringWhitespace("""
279+
280+
result.WithoutAnsi().WithoutDurations().Stdout.Should().BeEquivalentToIgnoringNewlines("""
238281
╭───────────────┬──────────┬───────────╮
239282
│ Resource │ Duration │ Status │
240283
├───────────────┼──────────┼───────────┤
241-
│ gridpointsReq │ <snip> │ Succeeded │
242-
│ gridCoords │ <snip> │ Succeeded │
284+
│ gridpointsReq │ │ Succeeded │
285+
│ gridCoords │ │ Succeeded │
243286
╰───────────────┴──────────┴───────────╯
244287
╭────────┬───────╮
245288
│ Output │ Value │
@@ -249,7 +292,4 @@ public async Task Local_deploy_with_azure_should_succeed(bool async)
249292
250293
""");
251294
}
252-
253-
private static string GetOutputWithoutDurations(string output)
254-
=> Regex.Replace(output, @"[ ]+\d+\.\d+s[ ]+", " <snip> ");
255295
}

0 commit comments

Comments
 (0)