Skip to content

Commit 1d559b3

Browse files
authored
Merge pull request #1521 from Azure/jcotillo/deployments_test
Enable schema loader test from Deployments Templates
2 parents 4011384 + 69ee9e3 commit 1d559b3

File tree

6 files changed

+247
-5
lines changed

6 files changed

+247
-5
lines changed

.github/workflows/main.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ jobs:
1414
uses: actions/setup-node@v2.1.2
1515
with:
1616
node-version: 14.x
17-
18-
- name: GitHub context (actor and head ref)
19-
run: |
20-
echo ${{ github.actor }}
21-
echo ${{ github.head_ref }}
17+
18+
- name: Setup .NET Core
19+
uses: actions/setup-dotnet@v1.7.2
2220

2321
- name: Install NPM modules
2422
run: npm ci
2523
working-directory: ./tools
2624

25+
- name: Deployments Schema tests
26+
run: dotnet test DeploymentsSchemaTests/DeploymentsSchemaTests.csproj
27+
working-directory: ./tools
28+
2729
- name: Run CI tests
2830
run: npm test
2931
working-directory: ./tools

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ node_modules
55
.DS_Store
66
test-results.xml
77
schemas/code-model-v1
8+
Debug
9+
Release
10+
obj
11+
bin
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<None Include="../../schemas/**" CopyToOutputDirectory="PreserveNewest" Link="schemas\%(RecursiveDir)%(Filename)%(Extension)" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Azure.Deployments.Templates" Version="1.0.154" />
15+
<PackageReference Include="FluentAssertions" Version="5.10.3" />
16+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
17+
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
18+
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
19+
<PackageReference Include="coverlet.collector" Version="1.3.0" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.31005.135
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "deploymentsSchemaTests", "deploymentsSchemaTests.csproj", "{3B03DF32-0523-4023-87DB-3C82BA8E0DCB}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{3B03DF32-0523-4023-87DB-3C82BA8E0DCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{3B03DF32-0523-4023-87DB-3C82BA8E0DCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{3B03DF32-0523-4023-87DB-3C82BA8E0DCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{3B03DF32-0523-4023-87DB-3C82BA8E0DCB}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {3A731ED6-FF9B-4887-A695-DA60235CA4AB}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using FluentAssertions;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using System;
4+
using System.IO;
5+
6+
namespace DeploymentsSchemaTests
7+
{
8+
[TestClass]
9+
public class DeploymentsTests
10+
{
11+
[TestMethod]
12+
public void TestSchemaLoader()
13+
{
14+
Action createAssemblyFunc = () =>
15+
TestSchemaCache.CreateFromFilePaths(
16+
filePaths: Directory.EnumerateFiles(
17+
path: "schemas",
18+
searchPattern: "*.json",
19+
searchOption: SearchOption.AllDirectories));
20+
createAssemblyFunc.Should().NotThrow();
21+
}
22+
}
23+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using Azure.Deployments.Core.Components;
2+
using Azure.Deployments.Core.Extensions;
3+
using Azure.Deployments.Core.Resources;
4+
using Azure.Deployments.Templates.Contracts;
5+
using Azure.Deployments.Templates.Export;
6+
using Azure.Deployments.Templates.Extensions;
7+
using Azure.Deployments.Templates.Helpers;
8+
using Azure.Deployments.TemplateSchemas;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
11+
using System;
12+
using System.Collections.Generic;
13+
using System.IO;
14+
using System.Linq;
15+
16+
namespace DeploymentsSchemaTests
17+
{
18+
/// <summary>
19+
/// Test schema cache
20+
/// </summary>
21+
internal class TestSchemaCache : INormalizedSchemaCache
22+
{
23+
/// <summary>
24+
/// Initializes the schema cache from a set of file paths.
25+
/// </summary>
26+
/// <param name="filePaths">The file paths to load.</param>
27+
public static INormalizedSchemaCache CreateFromFilePaths(IEnumerable<string> filePaths)
28+
{
29+
var schemaResults = TestSchemaCache.BuildSchemaCacheFromFilePaths(filePaths);
30+
31+
return new TestSchemaCache(schemaResults);
32+
}
33+
34+
private TestSchemaCache(ResultWithErrors<ResourceTypeSchema[]> schemaResults)
35+
{
36+
if (schemaResults.Errors.Any())
37+
{
38+
var errors = schemaResults.Errors
39+
.Where(err => !SchemaLoader.CircularReferenceSchemas.Contains(err.Target))
40+
.ToArray();
41+
42+
if (errors.Any())
43+
{
44+
var errorsJson = JsonConvert.SerializeObject(errors, Formatting.Indented);
45+
throw new InvalidOperationException($"Found errors building normalized cache: {errorsJson}");
46+
}
47+
}
48+
49+
this.schemaCache = schemaResults.Value
50+
.GroupByOrdinalInsensitively(schema => schema.ResourceProviderNamespace)
51+
.ToDictionary(
52+
keySelector: grouping => grouping.Key,
53+
elementSelector: grouping => grouping.ToLookupOrdinalInsensitively(schema => schema.FullyQualifiedResourceType),
54+
comparer: StringComparer.OrdinalIgnoreCase);
55+
}
56+
57+
private IReadOnlyDictionary<string, ILookup<string, ResourceTypeSchema>> schemaCache;
58+
59+
private class OfflineSchema : IResourceProviderSchema
60+
{
61+
public OfflineSchema(string providerNamespace, string apiVersion, JToken schemaContent)
62+
{
63+
this.ResourceProviderNamespace = providerNamespace;
64+
this.SchemaVersion = apiVersion;
65+
this.SchemaContent = schemaContent;
66+
}
67+
68+
public string ResourceProviderNamespace { get; }
69+
70+
public string SchemaVersion { get; }
71+
72+
public JToken SchemaContent { get; }
73+
}
74+
75+
private static readonly Uri SchemaBaseUri = new Uri("https://schema.management.azure.com/schemas/");
76+
77+
private static string GetRelativeSchemaPath(string schemaId)
78+
{
79+
var schemaUri = new Uri(new Uri(schemaId).GetLeftPart(UriPartial.Path));
80+
if (!SchemaBaseUri.IsBaseOf(schemaUri))
81+
{
82+
throw new ArgumentException($"Unable to process schema {schemaUri}");
83+
}
84+
85+
return SchemaBaseUri.MakeRelativeUri(schemaUri).ToString();
86+
}
87+
88+
private static ResultWithErrors<ResourceTypeSchema[]> BuildSchemaCacheFromFilePaths(IEnumerable<string> filePaths)
89+
{
90+
var schemasByPath = filePaths
91+
.Select(File.ReadAllText)
92+
.Select(JObject.Parse)
93+
.ToInsensitiveDictionary(
94+
keySelector: schema => GetRelativeSchemaPath(schema["id"].ToObject<string>()),
95+
elementSelector: schema => schema as JToken);
96+
97+
var externalReferenceSchemasResult = SchemaUtils.GetExternalReferenceSchemas(schemasByPath, SchemaUtils.ExternalReferenceWhitelist.ToArray());
98+
if (externalReferenceSchemasResult.Errors.Any())
99+
{
100+
throw new InvalidOperationException($"Failed to initialize the offline schemas cache");
101+
}
102+
103+
var schemaRefsByFile = SchemaUtils.TopLevelReferenceSchemas
104+
.SelectMany(schemaPath => SchemaUtils.GetExternalReferences(schemasByPath[schemaPath]))
105+
.Select(property => property.Value.ToObject<string>())
106+
.ToLookupOrdinalInsensitively(SchemaUtils.GetFilePathFromUri);
107+
108+
var schemaKeysFound = schemaRefsByFile
109+
.Select(grouping => grouping.Key)
110+
.Where(filePath => !SchemaUtils.ExternalReferenceWhitelist.Contains(filePath))
111+
.Where(filePath => schemasByPath.ContainsKey(filePath))
112+
.ToArray();
113+
114+
var schemaGroups = schemaKeysFound.GroupByInsensitively(keySelector: schemaKey => SchemaUtils.GetSchemaGroupKey(schemaKey));
115+
116+
var schemaNormalizationResults = schemaGroups
117+
.Select(schemaGroup => schemaGroup.ToInsensitiveDictionary(keySelector: schemaKey => schemaKey, elementSelector: schemaKey => schemasByPath[schemaKey]))
118+
.SelectMany(schemaGroup => SchemaUtils.NormalizeAndFilterSchemaGroup(
119+
schemaGroup: schemaGroup,
120+
externalReferenceSchemas: externalReferenceSchemasResult.Value,
121+
schemaRefsByFile: schemaRefsByFile))
122+
.ToInsensitiveDictionary(
123+
keySelector: normalizationResult => normalizationResult.Key,
124+
elementSelector: normalizationResult => normalizationResult.Value);
125+
126+
var resourceTypeSchemasResults = schemaNormalizationResults
127+
.Where(kvp => kvp.Value.Value != null)
128+
.Select(kvp => new OfflineSchema(
129+
providerNamespace: SchemaUtils.GetProviderNamespaceFromSchemaKey(kvp.Key),
130+
apiVersion: SchemaUtils.GetVersionFromSchemaKey(kvp.Key),
131+
schemaContent: kvp.Value.Value))
132+
.Select(schema => schema.GetResourceTypeSchemasResult());
133+
134+
return new ResultWithErrors<ResourceTypeSchema[]>
135+
{
136+
Value = resourceTypeSchemasResults.CollectValues().ToArray(),
137+
Errors = schemaNormalizationResults.Values.CollectErrors()
138+
.ConcatArray(resourceTypeSchemasResults.CollectErrors()),
139+
};
140+
}
141+
142+
/// <inheritdoc/>
143+
public ILookup<string, ResourceTypeSchema> GetSchemasForProvider(string providerNamespace)
144+
{
145+
if (this.schemaCache.TryGetValue(providerNamespace, out var schemaLookup))
146+
{
147+
return schemaLookup;
148+
}
149+
150+
return Enumerable.Empty<ResourceTypeSchema>().ToLookupOrdinalInsensitively(schema => schema.FullyQualifiedResourceType);
151+
}
152+
153+
/// <inheritdoc/>
154+
public IEnumerable<ResourceTypeSchema> GetSchemasForResourceType(string fullyQualifiedResourceType)
155+
{
156+
var providerNamespace = IResourceIdentifiableExtensions.GetNamespaceFromFullyQualifiedType(fullyQualifiedResourceType);
157+
158+
if (!this.schemaCache.TryGetValue(providerNamespace, out var schemaLookup))
159+
{
160+
return Enumerable.Empty<ResourceTypeSchema>();
161+
}
162+
163+
return schemaLookup[fullyQualifiedResourceType];
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)