src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs (223 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; using System.Text; using Bicep.Core.Emit; using Bicep.Core.FileSystem; using Bicep.Core.Parsing; using Bicep.Core.Samples; using Bicep.Core.Semantics; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Baselines; using Bicep.Core.UnitTests.Features; using Bicep.Core.UnitTests.Utils; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Bicep.Core.IntegrationTests.Emit { [TestClass] public class TemplateEmitterTests { private static ServiceBuilder Services => new ServiceBuilder() .WithEnvironmentVariables( ("stringEnvVariableName", "test"), ("intEnvVariableName", "100"), ("boolEnvironmentVariable", "true") ); [NotNull] public TestContext? TestContext { get; set; } private async Task<Compilation> GetCompilation(DataSet dataSet, FeatureProviderOverrides features) { // Use a unique cache root directory for each test run to avoid conflicts features = features with { CacheRootDirectory = FileHelper.GetCacheRootDirectory(TestContext) }; var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var clientFactory = dataSet.CreateMockRegistryClients(); var templateSpecRepositoryFactory = dataSet.CreateMockTemplateSpecRepositoryFactory(TestContext); await dataSet.PublishModulesToRegistryAsync(clientFactory); var bicepFilePath = Path.Combine(outputDirectory, DataSet.TestFileMain); var bicepFileUri = PathHelper.FilePathToFileUrl(bicepFilePath); var compiler = Services .WithContainerRegistryClientFactory(clientFactory) .WithTemplateSpecRepositoryFactory(templateSpecRepositoryFactory) .WithFeatureOverrides(features) .Build() .GetCompiler(); return await compiler.CreateCompilation(bicepFileUri); } [DataTestMethod] [DynamicData(nameof(GetValidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task ValidBicep_TemplateEmiterShouldProduceExpectedTemplate(DataSet dataSet) { var compiledFilePath = FileHelper.GetResultFilePath(this.TestContext, Path.Combine(dataSet.Name, DataSet.TestFileMainCompiled)); var compilation = await GetCompilation(dataSet, new()); var result = EmitTemplate(compilation, compiledFilePath); result.Diagnostics.Should().NotHaveErrors(); result.Status.Should().Be(EmitStatus.Succeeded); result.Features.Should().NotBeNull(); var outputFile = File.ReadAllText(compiledFilePath); var actual = JToken.Parse(outputFile); actual.Should().EqualWithJsonDiffOutput( TestContext, JToken.Parse(dataSet.Compiled!), expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainCompiled), actualLocation: compiledFilePath); // validate that the template is parseable by the deployment engine UnitTests.Utils.TemplateHelper.TemplateShouldBeValid(outputFile, result.Features!); } [DataTestMethod] [DynamicData(nameof(GetValidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task ValidBicep_EmitTemplate_should_produce_expected_symbolicname_template(DataSet dataSet) { var compiledFilePath = FileHelper.GetResultFilePath(this.TestContext, Path.Combine(dataSet.Name, DataSet.TestFileMainCompiledWithSymbolicNames)); var compilation = await GetCompilation(dataSet, new(SymbolicNameCodegenEnabled: true)); var result = EmitTemplate(compilation, compiledFilePath); result.Diagnostics.Should().NotHaveErrors(); result.Status.Should().Be(EmitStatus.Succeeded); result.Features.Should().NotBeNull(); var outputFile = File.ReadAllText(compiledFilePath); var actual = JToken.Parse(outputFile); actual.Should().EqualWithJsonDiffOutput( TestContext, JToken.Parse(dataSet.CompiledWithSymbolicNames!), expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainCompiledWithSymbolicNames), actualLocation: compiledFilePath); // validate that the template is parseable by the deployment engine UnitTests.Utils.TemplateHelper.TemplateShouldBeValid(outputFile, result.Features!); } [DataTestMethod] [EmbeddedFilesTestData(@"Files/SourceMapping/.*/main.bicep")] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task Source_map_generation_should_work(EmbeddedFile file) { var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, file); var bicepFile = baselineFolder.EntryFile; var sourceMapFile = baselineFolder.GetFileOrEnsureCheckedIn("sourcemap.json"); var features = new FeatureProviderOverrides(TestContext, SourceMappingEnabled: true); var compiler = ServiceBuilder.Create(s => s.WithFeatureOverrides(features)).GetCompiler(); var bicepUri = PathHelper.FilePathToFileUrl(bicepFile.OutputFilePath); var compilation = await compiler.CreateCompilation(bicepUri); var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); using var memoryStream = new MemoryStream(); var emitResult = emitter.Emit(memoryStream); emitResult.Status.Should().Be(EmitStatus.Succeeded); emitResult.SourceMap.Should().NotBeNull(); // Here we simply verify that the format of the baseline file looks correct. var sourceMapJson = JToken.FromObject(emitResult.SourceMap!); sourceMapFile.WriteToOutputFolder(sourceMapJson.ToString()); sourceMapFile.ShouldHaveExpectedJsonValue(); } [DataTestMethod] [DynamicData(nameof(GetValidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task SourceMap_maps_json_to_bicep_lines(DataSet dataSet) { var features = new FeatureProviderOverrides(TestContext, RegistryEnabled: dataSet.HasExternalModules, SourceMappingEnabled: true); var (compilation, outputDirectory, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext, features); var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); using var memoryStream = new MemoryStream(); var emitResult = emitter.Emit(memoryStream); emitResult.Status.Should().Be(EmitStatus.Succeeded); emitResult.SourceMap.Should().NotBeNull(); var sourceMap = emitResult.SourceMap!; using var streamReader = new StreamReader(new MemoryStream(memoryStream.ToArray())); var jsonLines = (await streamReader.ReadToEndAsync()).Split("\n"); var sourceTextWithSourceMap = OutputHelper.AddSourceMapToSourceText( dataSet.Bicep, DataSet.TestFileMain, dataSet.HasCrLfNewlines() ? "\r\n" : "\n", sourceMap, jsonLines); var sourceTextWithSourceMapFileName = Path.Combine(outputDirectory, DataSet.TestFileMainSourceMap); File.WriteAllText(sourceTextWithSourceMapFileName, sourceTextWithSourceMap.ToString()); // Here we validate visually that the in-memory source map can be used to map JSON -> Bicep lines sourceTextWithSourceMap.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.SourceMap!, expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSourceMap), actualPath: sourceTextWithSourceMapFileName); } [TestMethod] public void TemplateEmitter_output_should_not_include_UTF8_BOM() { var compilationResult = CompilationHelper.Compile(""); var compiledFilePath = FileHelper.GetResultFilePath(this.TestContext, "main.json"); // emitting the template should be successful var result = this.EmitTemplate(compilationResult.Compilation, compiledFilePath); result.Diagnostics.Should().BeEmpty(); result.Status.Should().Be(EmitStatus.Succeeded); var bytes = File.ReadAllBytes(compiledFilePath); // No BOM at the start of the file bytes.Take(3).Should().NotBeEquivalentTo(new[] { 0xEF, 0xBB, 0xBF }, "BOM should not be present"); bytes.First().Should().Be(0x7B, "template should always begin with a UTF-8 encoded open curly"); bytes.Last().Should().Be(0x7D, "template should always end with a UTF-8 encoded close curly"); } [DataTestMethod] [DynamicData(nameof(GetValidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task ValidBicepTextWriter_TemplateEmiterShouldProduceExpectedTemplate(DataSet dataSet) { var compilation = await GetCompilation(dataSet, new()); var memoryStream = new MemoryStream(); var result = this.EmitTemplate(compilation, memoryStream); result.Diagnostics.Should().NotHaveErrors(); result.Status.Should().Be(EmitStatus.Succeeded); // normalizing the formatting in case there are differences in indentation // this way the diff between actual and expected will be clean var actual = JToken.ReadFrom(new JsonTextReader(new StreamReader(new MemoryStream(memoryStream.ToArray())))); var compiledFilePath = FileHelper.SaveResultFile(this.TestContext, Path.Combine(dataSet.Name, DataSet.TestFileMainCompiled), actual.ToString(Formatting.Indented)); actual.Should().EqualWithJsonDiffOutput( TestContext, JToken.Parse(dataSet.Compiled!), expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainCompiled), actualLocation: compiledFilePath); } [DataTestMethod] [DynamicData(nameof(GetInvalidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] public async Task InvalidBicep_TemplateEmiterShouldNotProduceAnyTemplate(DataSet dataSet) { var compilation = await GetCompilation(dataSet, new()); string filePath = FileHelper.GetResultFilePath(this.TestContext, $"{dataSet.Name}_Compiled_Original.json"); // emitting the template should fail var result = this.EmitTemplate(compilation, filePath); result.Diagnostics.Should().NotBeEmpty(); result.Status.Should().Be(EmitStatus.Failed); } [DataTestMethod] [BaselineData_Bicepparam.TestData(Filter = BaselineData_Bicepparam.TestDataFilterType.ValidOnly)] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task Valid_bicepparam_TemplateEmiter_should_produce_expected_template(BaselineData_Bicepparam baselineData) { var data = baselineData.GetData(TestContext); data.Compiled.Should().NotBeNull(); var compiler = Services.Build().GetCompiler(); var compilation = await compiler.CreateCompilation(data.Parameters.OutputFileUri); var result = this.EmitParam(compilation, data.Compiled!.OutputFilePath); result.Diagnostics.Should().NotHaveErrors(); result.Status.Should().Be(EmitStatus.Succeeded); data.Compiled.ShouldHaveExpectedJsonValue(); } [DataTestMethod] [BaselineData_Bicepparam.TestData(Filter = BaselineData_Bicepparam.TestDataFilterType.InvalidOnly)] [TestCategory(BaselineHelper.BaselineTestCategory)] public async Task Invalid_bicepparam_TemplateEmiter_should_not_produce_a_template(BaselineData_Bicepparam baselineData) { var data = baselineData.GetData(TestContext); var compiler = Services.Build().GetCompiler(); var compilation = await compiler.CreateCompilation(data.Parameters.OutputFileUri); var result = this.EmitParam(compilation, Path.ChangeExtension(data.Parameters.OutputFilePath, ".json")); result.Diagnostics.Should().NotBeEmpty(); result.Status.Should().Be(EmitStatus.Failed); } [DataTestMethod] [DataRow("\n")] [DataRow("\r\n")] public void Multiline_strings_should_parse_correctly(string newlineSequence) { var inputFile = @" var multiline = ''' this is a multiline string ''' "; var (template, _, _) = CompilationHelper.Compile(StringUtils.ReplaceNewlines(inputFile, newlineSequence)); var expected = string.Join(newlineSequence, ["this", " is", " a", " multiline", " string", ""]); template.Should().HaveValueAtPath("$.variables.multiline", expected); } [TestMethod] public void TemplateEmitter_should_not_dispose_text_writer() { var (_, _, compilation) = CompilationHelper.Compile(string.Empty); var stringBuilder = new StringBuilder(); var stringWriter = new StringWriter(stringBuilder); var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); emitter.Emit(stringWriter); // second write should succeed if stringWriter wasn't closed emitter.Emit(stringWriter); } private EmitResult EmitTemplate(Compilation compilation, string filePath) { var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); using var stream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); return emitter.Emit(stream); } private EmitResult EmitTemplate(Compilation compilation, MemoryStream memoryStream) { var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); TextWriter tw = new StreamWriter(memoryStream); return emitter.Emit(tw); } private EmitResult EmitParam(Compilation compilation, string outputFilePath) { var emitter = new ParametersEmitter(compilation.GetEntrypointSemanticModel()); using var stream = new FileStream(outputFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); return emitter.Emit(stream); } private static IEnumerable<object[]> GetValidDataSets() => DataSets .AllDataSets .Where(ds => ds.IsValid) .ToDynamicTestData(); private static IEnumerable<object[]> GetInvalidDataSets() => DataSets .AllDataSets .Where(ds => ds.IsValid == false) .ToDynamicTestData(); } }