src/Bicep.Core.IntegrationTests/RegistryTests.cs (332 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Bicep.Core.Diagnostics; using Bicep.Core.FileSystem; using Bicep.Core.Registry; using Bicep.Core.Samples; using Bicep.Core.SourceGraph; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Utils; using Bicep.IO.Abstraction; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Bicep.Core.Samples.DataSet; // default t\o showing bicep source namespace Bicep.Core.IntegrationTests { [TestClass] public class RegistryTests { private static ServiceBuilder Services => new(); [NotNull] public TestContext? TestContext { get; set; } [TestMethod] public async Task InvalidRootCachePathShouldProduceReasonableErrors() { var dataSet = DataSets.Registry_LF; var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var clientFactory = dataSet.CreateMockRegistryClients(); var templateSpecRepositoryFactory = dataSet.CreateMockTemplateSpecRepositoryFactory(TestContext); await dataSet.PublishModulesToRegistryAsync(clientFactory); var fileUri = PathHelper.FilePathToFileUrl(Path.Combine(outputDirectory, DataSet.TestFileMain)); var badCacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists(); badCacheDirectory.GetFile("file.txt").EnsureExists(); badCacheDirectory = badCacheDirectory.GetDirectory("file.txt"); // cache root points to a file var featureOverrides = BicepTestConstants.FeatureOverrides with { RegistryEnabled = true, CacheRootDirectory = badCacheDirectory, }; var featuresFactory = BicepTestConstants.CreateFeatureProviderFactory(featureOverrides); var services = Services .WithFeatureOverrides(new(RegistryEnabled: true, CacheRootDirectory: badCacheDirectory)) .WithContainerRegistryClientFactory(clientFactory) .WithTemplateSpecRepositoryFactory(templateSpecRepositoryFactory) .Build(); var compiler = services.GetCompiler(); var compilation = await compiler.CreateCompilation(fileUri); var diagnostics = compilation.GetAllDiagnosticsByBicepFile(); diagnostics.Should().HaveCount(1); var expectedErrorMessage = "Unable to restore the artifact with reference \"{0}\": Unable to create the local artifact directory \""; diagnostics.Single().Value.ExcludingLinterDiagnostics().Should().SatisfyRespectively( x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:mock-registry-one.invalid/demo/plan:v2")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:mock-registry-one.invalid/demo/plan:v2")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:mock-registry-two.invalid/demo/site:v3")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:mock-registry-two.invalid/demo/site:v3")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "ts:11111111-1111-1111-1111-111111111111/prod-rg/vnet-spec:v2")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP062"); x.Message.Should().Be("The referenced declaration with name \"siteDeploy\" is not valid."); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:localhost:5000/passthrough/port:v1")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:127.0.0.1/passthrough/ipv4:v1")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:127.0.0.1:5000/passthrough/ipv4port:v1")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:[::1]/passthrough/ipv6:v1")); }, x => { x.Level.Should().Be(DiagnosticLevel.Error); x.Code.Should().Be("BCP192"); x.Message.Should().StartWith(string.Format(expectedErrorMessage, "br:[::1]:5000/passthrough/ipv6port:v1")); }); } [TestMethod] [DoNotParallelize()] public async Task ModuleRestoreContentionShouldProduceConsistentState() { var dataSet = DataSets.Registry_LF; var publishSource = true; var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var clientFactory = dataSet.CreateMockRegistryClients(); var templateSpecRepositoryFactory = dataSet.CreateMockTemplateSpecRepositoryFactory(TestContext); await dataSet.PublishModulesToRegistryAsync(clientFactory, publishSource); var cacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists(); var services = Services .WithFeatureOverrides(new(CacheRootDirectory: cacheDirectory)) .WithContainerRegistryClientFactory(clientFactory) .WithTemplateSpecRepositoryFactory(templateSpecRepositoryFactory) .Build(); var dispatcher = services.Construct<IModuleDispatcher>(); var dummyFile = CreateDummyReferencingFile(services); var moduleReferences = dataSet.RegistryModules.Values .OrderBy(m => m.Metadata.Target) .Select(m => TryGetModuleReference(dispatcher, dummyFile, m.Metadata.Target).Unwrap()) .ToImmutableList(); moduleReferences.Should().HaveCount(7); // initially the cache should be empty. foreach (var moduleReference in moduleReferences) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } const int ConcurrentTasks = 10; var tasks = new List<Task<bool>>(); for (int i = 0; i < ConcurrentTasks; i++) { tasks.Add(Task.Run(() => dispatcher.RestoreArtifacts(moduleReferences, forceRestore: false))); } var result = await Task.WhenAll(tasks); result.Should().HaveCount(ConcurrentTasks); // modules should now be in the cache foreach (var moduleReference in moduleReferences) { var restoreResult = dispatcher.GetArtifactRestoreStatus(moduleReference, out var errorBuilder); var error = errorBuilder?.Invoke(DiagnosticBuilder.ForDocumentStart()); restoreResult.Should().Be(ArtifactRestoreStatus.Succeeded, $"code: {error?.Code}, message: {error?.Message}"); } } [DataTestMethod] [DynamicData(nameof(GetModuleInfoData), DynamicDataSourceType.Method)] public async Task ModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnumerable<ExternalModuleInfo> moduleInfos, int moduleCount, bool publishSource) { var dataSet = DataSets.Registry_LF; var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var clientFactory = dataSet.CreateMockRegistryClients(); var templateSpecRepositoryFactory = dataSet.CreateMockTemplateSpecRepositoryFactory(TestContext); await dataSet.PublishModulesToRegistryAsync(clientFactory, publishSource: publishSource); var cacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists(); var fileResolver = BicepTestConstants.FileResolver; var services = Services .WithFeatureOverrides(new(CacheRootDirectory: cacheDirectory)) .WithContainerRegistryClientFactory(clientFactory) .WithTemplateSpecRepositoryFactory(templateSpecRepositoryFactory) .WithFileResolver(fileResolver) .Build(); var dispatcher = services.Construct<IModuleDispatcher>(); var dummyFile = CreateDummyReferencingFile(services); var moduleReferences = moduleInfos .OrderBy(m => m.Metadata.Target) .Select(m => TryGetModuleReference(dispatcher, dummyFile, m.Metadata.Target).IsSuccess(out var @ref) ? @ref : throw new AssertFailedException($"Invalid module target '{m.Metadata.Target}'.")) .ToImmutableList(); moduleReferences.Should().HaveCount(moduleCount); // initially the cache should be empty foreach (var moduleReference in moduleReferences) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } dispatcher.TryGetLocalArtifactEntryPointUri(moduleReferences[0]).IsSuccess(out var moduleFileUri).Should().BeTrue(); moduleFileUri.Should().NotBeNull(); var moduleFile = BicepTestConstants.FileExplorer.GetFile(IOUri.FromLocalFilePath(moduleFileUri!.LocalPath)); var moduleDirectory = moduleFile.GetParent().EnsureExists(); var lockFile = moduleDirectory.GetFile("lock"); var @lock = lockFile.TryLock(); @lock.Should().NotBeNull(); // let's try to restore a module while holding a lock using (@lock) { (await dispatcher.RestoreArtifacts(moduleReferences, forceRestore: false)).Should().BeTrue(); } // the first module should have failed due to a timeout dispatcher.GetArtifactRestoreStatus(moduleReferences[0], out var failureBuilder).Should().Be(ArtifactRestoreStatus.Failed); using (new AssertionScope()) { failureBuilder!.Should().HaveCode("BCP192"); failureBuilder!.Should().HaveMessageStartWith($"Unable to restore the artifact with reference \"{moduleReferences[0].FullyQualifiedReference}\": Exceeded the timeout of \"00:00:05\" to acquire the lock on file \""); } // all other modules should have succeeded foreach (var moduleReference in moduleReferences.Skip(1)) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Succeeded); } } [DataTestMethod] [DynamicData(nameof(GetModuleInfoData), DynamicDataSourceType.Method)] public async Task ForceModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnumerable<ExternalModuleInfo> moduleInfos, int moduleCount, bool publishSource) { var dataSet = DataSets.Registry_LF; var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var clientFactory = dataSet.CreateMockRegistryClients(); var templateSpecRepositoryFactory = dataSet.CreateMockTemplateSpecRepositoryFactory(TestContext); await dataSet.PublishModulesToRegistryAsync(clientFactory); var cacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists(); var fileResolver = BicepTestConstants.FileResolver; var services = Services .WithFeatureOverrides(new(CacheRootDirectory: cacheDirectory)) .WithContainerRegistryClientFactory(clientFactory) .WithTemplateSpecRepositoryFactory(templateSpecRepositoryFactory) .WithFileResolver(fileResolver) .Build(); var dispatcher = services.Construct<IModuleDispatcher>(); var dummyFile = CreateDummyReferencingFile(services); var moduleReferences = moduleInfos .OrderBy(m => m.Metadata.Target) .Select(m => TryGetModuleReference(dispatcher, dummyFile, m.Metadata.Target).IsSuccess(out var @ref) ? @ref : throw new AssertFailedException($"Invalid module target '{m.Metadata.Target}'.")) .ToImmutableList(); moduleReferences.Should().HaveCount(moduleCount); // initially the cache should be empty foreach (var moduleReference in moduleReferences) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } dispatcher.TryGetLocalArtifactEntryPointUri(moduleReferences[0]).IsSuccess(out var moduleFileUri).Should().BeTrue(); moduleFileUri.Should().NotBeNull(); var moduleFile = BicepTestConstants.FileExplorer.GetFile(IOUri.FromLocalFilePath(moduleFileUri!.LocalPath)); var moduleDirectory = moduleFile.GetParent().EnsureExists(); var lockFile = moduleDirectory.GetFile("lock"); var @lock = lockFile.TryLock(); @lock.Should().NotBeNull(); // let's try to restore a module while holding a lock using (@lock) { (await dispatcher.RestoreArtifacts(moduleReferences, forceRestore: true)).Should().BeTrue(); } // REF: FileLockTests.cs/FileLockShouldNotThrowIfLockFileIsDeleted() // Delete will succeed on Linux and Mac due to advisory nature of locks there using (new AssertionScope()) { #if WINDOWS_BUILD dispatcher.GetArtifactRestoreStatus(moduleReferences[0], out var failureBuilder).Should().Be(ArtifactRestoreStatus.Failed); failureBuilder!.Should().HaveCode("BCP233"); failureBuilder!.Should().HaveMessageStartWith($"Unable to delete the module with reference \"{moduleReferences[0].FullyQualifiedReference}\" from cache: Exceeded the timeout of \"00:00:05\" for the lock on file \"{lockFile.Uri}\" to be released."); #else dispatcher.GetArtifactRestoreStatus(moduleReferences[0], out _).Should().Be(ArtifactRestoreStatus.Succeeded); #endif // all other modules should have succeeded foreach (var moduleReference in moduleReferences.Skip(1)) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Succeeded); } } } [DataTestMethod] [DynamicData(nameof(GetModuleInfoData), DynamicDataSourceType.Method)] public async Task ForceModuleRestoreShouldRestoreAllModules(IEnumerable<ExternalModuleInfo> moduleInfos, int moduleCount, bool publishSource) { var dataSet = DataSets.Registry_LF; var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var clientFactory = dataSet.CreateMockRegistryClients(); var templateSpecRepositoryFactory = dataSet.CreateMockTemplateSpecRepositoryFactory(TestContext); await dataSet.PublishModulesToRegistryAsync(clientFactory, publishSource); var cacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists(); var fileResolver = BicepTestConstants.FileResolver; var services = Services .WithFeatureOverrides(new(CacheRootDirectory: cacheDirectory)) .WithContainerRegistryClientFactory(clientFactory) .WithTemplateSpecRepositoryFactory(templateSpecRepositoryFactory) .WithFileResolver(fileResolver) .Build(); var dispatcher = services.Construct<IModuleDispatcher>(); var dummyFile = CreateDummyReferencingFile(services); var moduleReferences = moduleInfos .OrderBy(m => m.Metadata.Target) .Select(m => TryGetModuleReference(dispatcher, dummyFile, m.Metadata.Target).IsSuccess(out var @ref) ? @ref : throw new AssertFailedException($"Invalid module target '{m.Metadata.Target}'.")) .ToImmutableList(); moduleReferences.Should().HaveCount(moduleCount); // initially the cache should be empty foreach (var moduleReference in moduleReferences) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } dispatcher.TryGetLocalArtifactEntryPointUri(moduleReferences[0]).IsSuccess(out var moduleFileUri).Should().BeTrue(); moduleFileUri.Should().NotBeNull(); var moduleFilePath = moduleFileUri!.LocalPath; var moduleDirectory = Path.GetDirectoryName(moduleFilePath)!; Directory.CreateDirectory(moduleDirectory); (await dispatcher.RestoreArtifacts(moduleReferences, forceRestore: true)).Should().BeTrue(); // all other modules should have succeeded foreach (var moduleReference in moduleReferences) { dispatcher.GetArtifactRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Succeeded); } } public static IEnumerable<object[]> GetModuleInfoData() { yield return new object[] { DataSets.Registry_LF.RegistryModules.Values, 7, false /* publishSource */ }; yield return new object[] { DataSets.Registry_LF.RegistryModules.Values, 7, true }; yield return new object[] { DataSets.Registry_LF.TemplateSpecs.Values, 2, false }; yield return new object[] { DataSets.Registry_LF.TemplateSpecs.Values, 2, true }; } private static BicepFile CreateDummyReferencingFile(IDependencyHelper dependencyHelper) { var sourceFileFactory = dependencyHelper.Construct<ISourceFileFactory>(); return sourceFileFactory.CreateBicepFile(new Uri("inmemory:///main.bicep"), ""); } private static ResultWithDiagnosticBuilder<ArtifactReference> TryGetModuleReference(IModuleDispatcher moduleDispatcher, BicepSourceFile referencingFile, string reference) => moduleDispatcher.TryGetArtifactReference(referencingFile, ArtifactType.Module, reference); } }