Skip to content

Commit

Permalink
Add test framework to evaluate a template containing expressions (#1875)
Browse files Browse the repository at this point in the history
* Add framework to evaluate a template

* Add more evaluation tests
  • Loading branch information
anthony-c-martin authored Mar 16, 2021
1 parent 676dbf8 commit 1bd5ffc
Show file tree
Hide file tree
Showing 3 changed files with 452 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Deployments.Templates" Version="1.0.162" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.2" />
Expand Down
262 changes: 262 additions & 0 deletions src/Bicep.Core.IntegrationTests/EvaluationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;

namespace Bicep.Core.IntegrationTests
{
[TestClass]
public class EvaluationTests
{
[TestMethod]
public void Basic_arithmetic_expressions_are_evaluated_successfully()
{
var (template, _, _) = CompilationHelper.Compile(@"
var sum = 1 + 3
var mult = sum * 5
var modulo = mult % 7
var div = modulo / 11
var sub = div - 13
output sum int = sum
output mult int = mult
output modulo int = modulo
output div int = div
output sub int = sub
");

using (new AssertionScope())
{
var evaluated = TemplateEvaluator.Evaluate(template);

evaluated.Should().HaveValueAtPath("$.outputs['sum'].value", 4);
evaluated.Should().HaveValueAtPath("$.outputs['mult'].value", 20);
evaluated.Should().HaveValueAtPath("$.outputs['modulo'].value", 6);
evaluated.Should().HaveValueAtPath("$.outputs['div'].value", 0);
evaluated.Should().HaveValueAtPath("$.outputs['sub'].value", -13);
}
}

[TestMethod]
public void String_expressions_are_evaluated_successfully()
{
var (template, _, _) = CompilationHelper.Compile(@"
var bool = false
var int = 12948
var str = 'hello!'
var obj = {
a: 'b'
'!c': 2
}
var arr = [
true
2893
'abc'
]
var multiline = '''
these escapes
are not
evaluted:
\r\n\t\\\'\${}
'''
output literal string = str
output interp string = '>${bool}<>${int}<>${str}<>${obj}<>${arr}<'
output escapes string = '\r\n\t\\\'\${}'
output multiline string = multiline
");

using (new AssertionScope())
{
var evaluated = TemplateEvaluator.Evaluate(template);

evaluated.Should().HaveValueAtPath("$.outputs['literal'].value", "hello!");
evaluated.Should().HaveValueAtPath("$.outputs['interp'].value", ">False<>12948<>hello!<>{'a':'b','!c':2}<>[true,2893,'abc']<");
evaluated.Should().HaveValueAtPath("$.outputs['escapes'].value", "\r\n\t\\'${}");
evaluated.Should().HaveValueAtPath("$.outputs['multiline'].value", "these escapes\n are not\n evaluted:\n\\r\\n\\t\\\\\\'\\${}\n");
}
}

[TestMethod]
public void ResourceId_expressions_are_evaluated_successfully()
{
var (template, _, _) = CompilationHelper.Compile(@"
param parentName string
param childName string
resource existing1 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
}
resource existing2 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: resourceGroup('customRg')
}
resource existing3 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: resourceGroup('2e518a80-f860-4467-a00e-d81aaf1ff42e', 'customRg')
}
resource existing4 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: tenant()
}
resource existing5 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: subscription()
}
resource existing6 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: subscription('2e518a80-f860-4467-a00e-d81aaf1ff42e')
}
resource existing7 'My.Rp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: managementGroup('2e518a80-f860-4467-a00e-d81aaf1ff42e')
}
resource existing8 'My.ExtensionRp/parent/child@2020-01-01' existing = {
name: '${parentName}/${childName}'
scope: existing4
}
output resource1Id string = existing1.id
output resource2Id string = existing2.id
output resource3Id string = existing3.id
output resource4Id string = existing4.id
output resource5Id string = existing5.id
output resource6Id string = existing6.id
output resource7Id string = existing7.id
output resource8Id string = existing8.id
output resource1Name string = existing1.name
output resource1ApiVersion string = existing1.apiVersion
output resource1Type string = existing1.type
");

using (new AssertionScope())
{
var testSubscriptionId = "87d64d6d-6d17-4ad7-b507-16d9bc498781";
var testRgName = "testRg";
var evaluated = TemplateEvaluator.Evaluate(template, config => config with {
SubscriptionId = testSubscriptionId,
ResourceGroup = testRgName,
Parameters = new() {
["parentName"] = "myParent",
["childName"] = "myChild",
}
});

evaluated.Should().HaveValueAtPath("$.outputs['resource1Id'].value", $"/subscriptions/{testSubscriptionId}/resourceGroups/{testRgName}/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource2Id'].value", $"/subscriptions/{testSubscriptionId}/resourceGroups/customRg/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource3Id'].value", $"/subscriptions/2e518a80-f860-4467-a00e-d81aaf1ff42e/resourceGroups/customRg/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource4Id'].value", $"/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource5Id'].value", $"/subscriptions/{testSubscriptionId}/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource6Id'].value", $"/subscriptions/2e518a80-f860-4467-a00e-d81aaf1ff42e/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource7Id'].value", $"/providers/Microsoft.Management/managementGroups/2e518a80-f860-4467-a00e-d81aaf1ff42e/providers/My.Rp/parent/myParent/child/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource8Id'].value", $"/providers/My.Rp/parent/myParent/child/myChild/providers/My.ExtensionRp/parent/myParent/child/myChild");

evaluated.Should().HaveValueAtPath("$.outputs['resource1Name'].value", "myParent/myChild");
evaluated.Should().HaveValueAtPath("$.outputs['resource1ApiVersion'].value", "2020-01-01");
evaluated.Should().HaveValueAtPath("$.outputs['resource1Type'].value", "My.Rp/parent/child");
}
}

[TestMethod]
public void Comparisons_are_evaluated_correctly()
{
var (template, _, _) = CompilationHelper.Compile(@"
output less bool = 123 < 456
output lessOrEquals bool = 123 <= 456
output greater bool = 123 > 456
output greaterOrEquals bool = 123 >= 456
output equals bool = 123 == 456
output not bool = !true
output and bool = true && false
output or bool = true || false
output coalesce int = null ?? 123
");

using (new AssertionScope())
{
var evaluated = TemplateEvaluator.Evaluate(template);

evaluated.Should().HaveValueAtPath("$.outputs['less'].value", true);
evaluated.Should().HaveValueAtPath("$.outputs['lessOrEquals'].value", true);
evaluated.Should().HaveValueAtPath("$.outputs['greater'].value", false);
evaluated.Should().HaveValueAtPath("$.outputs['greaterOrEquals'].value", false);
evaluated.Should().HaveValueAtPath("$.outputs['equals'].value", false);
evaluated.Should().HaveValueAtPath("$.outputs['not'].value", false);
evaluated.Should().HaveValueAtPath("$.outputs['and'].value", false);
evaluated.Should().HaveValueAtPath("$.outputs['or'].value", true);
evaluated.Should().HaveValueAtPath("$.outputs['coalesce'].value", 123);
}
}

[TestMethod]
public void Resource_property_access_works()
{
var (template, _, _) = CompilationHelper.Compile(@"
param abcVal string
resource testRes 'My.Rp/res1@2020-01-01' = {
name: 'testRes'
properties: {
abc: abcVal
}
}
output abcVal string = testRes.properties.abc
");

using (new AssertionScope())
{
var evaluated = TemplateEvaluator.Evaluate(template, config => config with {
Parameters = new() {
["abcVal"] = "test!!!",
},
});

evaluated.Should().HaveValueAtPath("$.outputs['abcVal'].value", "test!!!");
}
}

[TestMethod]
public void Existing_resource_property_access_works()
{
var (template, _, _) = CompilationHelper.Compile(@"
resource testRes 'My.Rp/res1@2020-01-01' existing = {
name: 'testRes'
}
output abcVal string = testRes.properties.abc
");

using (new AssertionScope())
{
var evaluated = TemplateEvaluator.Evaluate(template, config => config with {
OnReferenceFunc = (resourceId, apiVersion, fullBody) => {
if (resourceId == $"/subscriptions/{config.SubscriptionId}/resourceGroups/{config.ResourceGroup}/providers/My.Rp/res1/testRes" && apiVersion == "2020-01-01" && !fullBody)
{
return new JObject {
["abc"] = "test!!!",
};
}

throw new NotImplementedException();
},
});

evaluated.Should().HaveValueAtPath("$.outputs['abcVal'].value", "test!!!");
}
}
}
}
Loading

0 comments on commit 1bd5ffc

Please sign in to comment.